diff --git a/HuffmanCompressorCmd/AssemblyInfo.cs b/HuffmanCompressor.Cmd/AssemblyInfo.cs similarity index 93% rename from HuffmanCompressorCmd/AssemblyInfo.cs rename to HuffmanCompressor.Cmd/AssemblyInfo.cs index 87a6d0f..26d78a2 100644 --- a/HuffmanCompressorCmd/AssemblyInfo.cs +++ b/HuffmanCompressor.Cmd/AssemblyInfo.cs @@ -17,5 +17,5 @@ [assembly: Guid("29445467-fd04-458f-9bd4-fcd842b0468d")] -[assembly: InternalsVisibleTo("HuffmanCompressorTests")] +[assembly: InternalsVisibleTo("HuffmanCompressor.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/HuffmanCompressorCmd/HuffmanCompressorCmd.csproj b/HuffmanCompressor.Cmd/HuffmanCompressor.Cmd.csproj similarity index 56% rename from HuffmanCompressorCmd/HuffmanCompressorCmd.csproj rename to HuffmanCompressor.Cmd/HuffmanCompressor.Cmd.csproj index d1a7347..83fdd4d 100644 --- a/HuffmanCompressorCmd/HuffmanCompressorCmd.csproj +++ b/HuffmanCompressor.Cmd/HuffmanCompressor.Cmd.csproj @@ -8,7 +8,11 @@ - + + + + + diff --git a/HuffmanCompressor.Cmd/Program.cs b/HuffmanCompressor.Cmd/Program.cs new file mode 100644 index 0000000..964ed06 --- /dev/null +++ b/HuffmanCompressor.Cmd/Program.cs @@ -0,0 +1,80 @@ +namespace HuffmanCompressor.Cmd; + +using System; +using Microsoft.Extensions.DependencyInjection; +using HuffmanCompressor.Lib; + +/// +/// Main entry point for the HuffmanCompressorCmd console application. +/// +public class Program +{ + private IFileCompressor _compressor; + + /// + /// Constructor of the Program class. + /// + public Program(IFileCompressor compressor) + { + _compressor = compressor; + } + + /// + /// The main entry point (Main method) for the application. + /// + /// Program arguments. + public static void Main(string[] args) + { + var serviceProvider = ConfigureServices(); + var program = new Program(serviceProvider.GetRequiredService()); + program.Run(args); + } + + /// + /// The main entry point (Main method) for the application always needs to be declared as static. + /// That means that it cannot access non-static members of the class, such as _compressor. + /// Declaring the _compressor member as static would create a single instance of the compressor for all instances of the Program class. + /// This would be a problem if the Program class was used in a multi-threaded environment, such as when unit tests are run, where we + /// may run parallel compression jobs or even want to inject a mock compressor for testing purposes. + /// To avoid this, we create the Run wrapper as non-static and have Main create an instance of the Program class to invoke it. + /// + /// Program args + /// + public void Run(string[] args) + { + const string usageString = "Usage: [compress|inflate] [input file path] [output file path]"; + + if (args.Length != 3) + { + Console.WriteLine(usageString); + throw new ArgumentException(usageString); + } + + switch (args[0].ToLower()) + { + case "compress": + _compressor.Compress(args[1], args[2]); + break; + case "inflate": + _compressor.Inflate(args[1], args[2]); + break; + default: + Console.WriteLine(usageString); + throw new ArgumentException(usageString); + } + } + + /// + /// Configures dependency injection container. + /// + /// Service provider with registered services. + private static IServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + + // Register IFileCompressor with HuffmanCompressor + services.AddTransient(); + + return services.BuildServiceProvider(); + } +} \ No newline at end of file diff --git a/HuffmanCompressor.Lib/BitReader.cs b/HuffmanCompressor.Lib/BitReader.cs new file mode 100644 index 0000000..9d1fb35 --- /dev/null +++ b/HuffmanCompressor.Lib/BitReader.cs @@ -0,0 +1,49 @@ +namespace HuffmanCompressor.Lib; + +/// +/// Provides functionality to read individual bits from an input file stream. +/// +internal class BitReader +{ + private int bitIndex; + private byte currentByte; + private readonly FileStream fileHandle; + + /// + /// Creates an instance of class. + /// + /// File stream to which the reader will read bits from. + public BitReader(FileStream fileHandle) + { + ArgumentNullException.ThrowIfNull(fileHandle); + this.bitIndex = 8; + this.currentByte = 0x00; + this.fileHandle = fileHandle; + } + + /// + /// Reads a bit from the file. + /// + /// Bit represented as a '0' or '1' character. + /// If the end of the stream is reached while attempting to read. + public char ReadNextBit() + { + // Read 1 byte from the input file stream at a time, and use an index to return each bit from the byte. + // When we get to the last bit, read the next byte and continue until end of file + if (this.bitIndex >= 8) + { + var byteValue = this.fileHandle.ReadByte(); + if (byteValue == -1) + { + throw new EndOfStreamException("Attempting to read past the end of the stream!"); + } + this.currentByte = (byte)byteValue; + this.bitIndex = 0; + } + + byte mask = (byte)(0x80 >> this.bitIndex++); + return (this.currentByte & mask) == 0x00 + ? '0' + : '1'; + } +} diff --git a/HuffmanCompressor.Lib/BitWriter.cs b/HuffmanCompressor.Lib/BitWriter.cs new file mode 100644 index 0000000..db23ade --- /dev/null +++ b/HuffmanCompressor.Lib/BitWriter.cs @@ -0,0 +1,76 @@ +namespace HuffmanCompressor.Lib; + +/// +/// Provides functionality to write individual bits to an output file stream. +/// +internal class BitWriter +{ + private int bitIndex; + private byte currentByte; + private readonly FileStream fileHandle; + + /// + /// Creates an instance of class. + /// + /// File stream to which the writer will write bits to. + public BitWriter(FileStream fileHandle) + { + ArgumentNullException.ThrowIfNull(fileHandle); + this.bitIndex = 0; + this.currentByte = 0x00; + this.fileHandle = fileHandle; + } + + /// + /// Writes bits to the output file. + /// + /// String of bits, only 0 and 1 are supported. + /// In case the bit string contains unsupported characters. + public void WriteBits(string bitString) + { + ArgumentNullException.ThrowIfNull(bitString); + + // Fill in currentByte from most significant bit to least significant bit by reading from input bitString + // until the input bitString is exhausted. + // As we fill out all available bits in currentByte, we flush it out to the file and start over with a fresh byte + for (int i = 0; i < bitString.Length; i++) + { + var bit = bitString[i]; + switch(bit) + { + case '0': + break; + case '1': + // mask a '1' on the current byte + byte mask = 0x80; + mask = (byte)(mask >> bitIndex); + currentByte |= mask; + break; + default: + throw new ArgumentException($"Invalid bit detected {bit}"); + } + + if (bitIndex == 7) + { + // currentByte has been exhausted for all of its 8 available bits + // commit to disk and mint a new byte to continue the process. + fileHandle.WriteByte(currentByte); + bitIndex = 0; + currentByte = 0x00; + } + else + { + bitIndex++; + } + } + } + + /// + /// Write the outstanding byte to disk, prior to closing the file handle. + /// The remaining bits will be padding. + /// + public void Flush() + { + fileHandle.WriteByte(currentByte); + } +} diff --git a/HuffmanCompressor.Lib/FrequencyCounter.cs b/HuffmanCompressor.Lib/FrequencyCounter.cs new file mode 100644 index 0000000..08631f9 --- /dev/null +++ b/HuffmanCompressor.Lib/FrequencyCounter.cs @@ -0,0 +1,126 @@ +namespace HuffmanCompressor.Lib; + +/// +/// Class to count the relative frequency of each byte in a stream of data. +/// There is no upper bound on the number of bytes that can be counted. +/// If the counter reaches the maximum value, the class will rebase all of the frequencies to avoid overflows. +/// +internal class FrequencyCounter +{ + private readonly IDictionary moduloCounter; + private readonly IDictionary frequencies; + private int multiplier; + + /// + /// Initializes a new instance of the class. + /// + public FrequencyCounter() + { + this.moduloCounter = new Dictionary(); + this.frequencies = new Dictionary(); + multiplier = 1; + + // Initialize the dictionaries to make the code more straightforward in the rest of the class. + // Avoid overflow of counter that would lead to an infinite loop using an int + // https://stackoverflow.com/questions/43800147/iterating-from-minvalue-to-maxvalue-with-overflow + for (int i = byte.MinValue; i <= byte.MaxValue; i++) + { + this.moduloCounter.Add((byte)i, 0); + this.frequencies.Add((byte)i, 0); + } + } + + /// + /// Increment the frequency of a byte by 1. + /// + /// The value to increment the frequency of. + public void Increment(byte value) + { + // Check to see if we have reached the rare theoretic limit for frequency counting. + // In that case, the best we can do is stop counting to avoid an overlfow back to 0. + // Note: this would still mean having read 16 million TB of a single character! + if (this.moduloCounter[value] == UInt32.MaxValue && + this.frequencies[value] == UInt32.MaxValue) + { + return; + } + + this.moduloCounter[value]++; + if (this.moduloCounter[value] == multiplier) + { + // If the counter is about to overflow, we call the Rebase function + if (this.frequencies[value] == UInt32.MaxValue) + { + this.Rebase(); + } + else + { + this.frequencies[value]++; + } + + this.moduloCounter[value] = 0; + } + } + + /// + /// Set the frequency of a byte to a specific value. + /// + /// The value to set. + /// The frequency to set. + public void SetFrequency(byte value, UInt32 frequency) + { + this.frequencies[(byte)value] = frequency; + this.moduloCounter[(byte)value] = 0; + } + + /// + /// Get the frequency of a byte. + /// + /// The value to query the frequency of + /// Frequency of the value. + public UInt32 GetFrequency(byte value) + { + // First check for the case that the frequency counter has not yet been + // bumped up, in that case, if we ever counted the value in the modulo counter, we must return + // the smallest value possible, which is 1. + if (this.frequencies[value] == 0) + { + if (this.moduloCounter[value] > 0) + { + return 1; + } + + return 0; + } + + return this.frequencies[value]; + } + + /// + /// Enumerator to allow caller to cycle through frequencies. + /// + /// Key value pair enumaration of non-zero frequencies + public IEnumerable> GetEnumerator() + { + for (int b = byte.MinValue; b <= byte.MaxValue; b++) + { + var frequency = this.GetFrequency((byte)b); + if (frequency > 0) + { + yield return new KeyValuePair((byte)b, frequency); + } + } + } + + /// + /// Rebases all of the frequencies uniformly by dividing them by 2 + /// This allows us to handle very high counts without risk of overflowing and still + /// preserving statistical accuracy. + /// + private void Rebase() + { + this.frequencies.ToList().ForEach( kvp => this.frequencies[kvp.Key] /= 2 + 1); + this.multiplier *= 2; + } + +} diff --git a/HuffmanCompressor/HuffmanCompressorLib.csproj b/HuffmanCompressor.Lib/HuffmanCompressor.Lib.csproj similarity index 100% rename from HuffmanCompressor/HuffmanCompressorLib.csproj rename to HuffmanCompressor.Lib/HuffmanCompressor.Lib.csproj diff --git a/HuffmanCompressor.Lib/HuffmanCompressor.cs b/HuffmanCompressor.Lib/HuffmanCompressor.cs new file mode 100644 index 0000000..6f54234 --- /dev/null +++ b/HuffmanCompressor.Lib/HuffmanCompressor.cs @@ -0,0 +1,251 @@ +namespace HuffmanCompressor.Lib; + +using System; +using System.Collections.Generic; +using System.Linq; + +/// +/// Class that implements the Huffman compression algorithm. +/// +public class HuffmanCompressor : IFileCompressor +{ + private FrequencyCounter frequencyCounter; + private IDictionary binaryCodeMappings; + private Node? treeRoot; + + /// + /// Code to mark End of File character. Must not be a confused with a regular byte 0 to 255. + /// + private const short EndOfFileCode = -1; + + /// + /// Initializes a new instance of the class. + /// + public HuffmanCompressor() + { + this.frequencyCounter = new FrequencyCounter(); + this.binaryCodeMappings = new Dictionary(); + } + + /// + /// Compresses a file. + /// + /// File to be compressed. + /// Path of destination (compressed) file. + /// is . + /// is . + + public void Compress(string inputFilePath, string outputFilePath) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(inputFilePath); + ArgumentNullException.ThrowIfNullOrWhiteSpace(outputFilePath); + + this.InitializeFrequencyDictionary(inputFilePath); + this.BuildTree(); + this.BuildBinaryCodeMappings(); + this.CompressInternal(inputFilePath, outputFilePath); + } + + /// + /// Inflates a file. + /// + /// File to be inflated. + /// Path of destination (inflated) file. + /// is . + /// is . + + public void Inflate(string inputFilePath, string outputFilePath) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(inputFilePath); + ArgumentNullException.ThrowIfNullOrWhiteSpace(outputFilePath); + + var inputStream = this.ReadFrequencyDictionary(inputFilePath); + this.BuildTree(); + this.BuildBinaryCodeMappings(); + this.InflateInternal(inputStream, outputFilePath); + } + + private void InitializeFrequencyDictionary(string inputFilePath) + { + this.frequencyCounter = new FrequencyCounter(); + using (var inputStream = File.OpenRead(inputFilePath)) + { + int inputByte; + // -1 represents end of stream, otherwise byte cast as int + while ((inputByte = inputStream.ReadByte()) != -1) + { + byte nativeByte = (byte)inputByte; + this.frequencyCounter.Increment(nativeByte); + } + } + } + + private void BuildTree() + { + // Use a MinHeap priority queue, creating a node with the byte, and the frequency as priority + var priorityQueue = new PriorityQueue, UInt32>(); + foreach (var kvp in frequencyCounter.GetEnumerator()) + { + priorityQueue.Enqueue(new Node(kvp.Key), kvp.Value); + } + + // Enque special "end of file" node with value -1 and priority 0. This special marker will appear at the end of the file so that during + // decompression we deterministically know we have exhausted the input file. This allows us to compress arbitrarily large input files without having to save in + // the file header the number of bytes in the input, which could overflow for very large files. + priorityQueue.Enqueue(new Node(EndOfFileCode), 0); + + // Take the 2 nodes at the head of the queue (lowest frequency) and combine into a new node + // The tree is complete when there is only 1 node left + while (priorityQueue.Count > 1) + { + priorityQueue.TryDequeue(out var leftNode, out UInt32 leftPriority); + priorityQueue.TryDequeue(out var rightNode, out UInt32 rightPriority); + priorityQueue.Enqueue(new Node(leftNode!, rightNode!), leftPriority + rightPriority); + } + + this.treeRoot = priorityQueue.Dequeue(); + } + + private void BuildBinaryCodeMappings() + { + this.binaryCodeMappings = new Dictionary(); + BuildBinaryCodeMappings(this.treeRoot!, string.Empty); + } + + private void BuildBinaryCodeMappings(Node node, string binaryCode) + { + if (node.IsLeafNode) + { + this.binaryCodeMappings!.Add(node.Value, binaryCode); + return; + } + + // Left node gets tagged with 0, right node gets tagged with 1 + this.BuildBinaryCodeMappings(node.GetLeft()!, $"{binaryCode}0"); + this.BuildBinaryCodeMappings(node.GetRight()!, $"{binaryCode}1"); + } + + private void CompressInternal(string inputFilePath, string outputFilePath) + { + using (var inputStream = File.OpenRead(inputFilePath)) + { + // Open a filestream to the destination (compressed) file. If the file already exists, it will be overwritten. + using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + { + this.WriteFrequencyDictionary(outputStream); + + var bitWriter = new BitWriter(outputStream); + + // Main loop to encode each byte in the input stream according to its binary code mapping + int inputByte; + while ((inputByte = inputStream.ReadByte()) != -1) + { + bitWriter.WriteBits(this.binaryCodeMappings![(byte)inputByte]); + } + + // Write End of file code at the very end. + bitWriter.WriteBits(this.binaryCodeMappings![EndOfFileCode]); + + bitWriter.Flush(); + } + } + } + + /// + /// Writes the frequency dictionary to the output file so that the binary tree can be rebuilt to decompress the file. + /// As an optimization, we only write frequencies for the bytes that were present in the input file. + /// Note: storing the binary code mappings could be used as an alternate way to decode the file, but it would consume more disk space than the frequencies table + /// defeating the purpose of an efficient compression algorithm. Instead, the binary code mappings will be rebuilt at time of decompression from this frequency table. + /// + /// + private void WriteFrequencyDictionary(FileStream outputStream) + { + var writer = new BinaryWriter(outputStream); + + // frequency table size can be 256 + 1 end of file character (257) at most, so we need 2 bytes at most (ushort) on the header of size of frequency table + writer.Write((ushort)this.frequencyCounter.GetEnumerator().Count()); + foreach (var kvp in this.frequencyCounter.GetEnumerator()) + { + writer.Write(kvp.Key); + writer.Write(kvp.Value); + } + writer.Flush(); + } + + private FileStream ReadFrequencyDictionary(string inputFilePath) + { + this.frequencyCounter = new FrequencyCounter(); + FileStream inputStream; + try + { + inputStream = File.OpenRead(inputFilePath); + } + catch (Exception e) + { + Console.WriteLine($"Exception opening input file: {e.Message}"); + throw; + } + + var reader = new BinaryReader(inputStream); + var frequencyTableSize = reader.ReadUInt16(); + // 256 = all possible 8 bit characters + if (frequencyTableSize < 0 || frequencyTableSize > 256) + { + throw new ArgumentOutOfRangeException($"Invalid frequency count {frequencyTableSize} in input file"); + } + + for (int i = 0; i < frequencyTableSize; i++) + { + var key = reader.ReadByte(); + var value = reader.ReadUInt32(); + this.frequencyCounter.SetFrequency(key, value); + } + + // Hand off the input stream to the next steps of decompression so they continue reading after the frequency dictionary header + return inputStream; + } + + private void InflateInternal(FileStream inputStream, string outputFilePath) + { + // Open a filestream to the destination (uncompressed) file. If the file already exists, it will be overwritten. + using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) + { + // Special case if input file was empty, nothing to do + if (this.frequencyCounter.GetEnumerator().Count() == 0) + { + return; + } + + // For decompression, we key off binary codes to obtain the corresponding byte, which is the opposite to what we do in compression. + // This ensures that lookup for decompression has constant time complexity. + var reverseBinaryCodeMappings = this.binaryCodeMappings?.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); + var bitReader = new BitReader(inputStream); + var endOfFileFound = false; + while (!endOfFileFound) + { + var bitString = string.Empty; + var binaryCodeMatch = false; + while (!binaryCodeMatch) + { + var bit = bitReader.ReadNextBit(); + bitString = $"{bitString}{bit}"; + if (reverseBinaryCodeMappings!.ContainsKey(bitString)) + { + binaryCodeMatch = true; + if (reverseBinaryCodeMappings![bitString] == EndOfFileCode) + { + endOfFileFound = true; + } + else + { + // A matching pattern in the binary code mappings is found, write the corresponding byte to the output + outputStream.WriteByte((byte)reverseBinaryCodeMappings[bitString]); + binaryCodeMatch = true; + } + } + } + } + inputStream.Close(); + } + } +} \ No newline at end of file diff --git a/HuffmanCompressor.Lib/IFileCompressor.cs b/HuffmanCompressor.Lib/IFileCompressor.cs new file mode 100644 index 0000000..018a86a --- /dev/null +++ b/HuffmanCompressor.Lib/IFileCompressor.cs @@ -0,0 +1,10 @@ +namespace HuffmanCompressor.Lib; + +/// +/// Interface for file compression and decompression. +/// +public interface IFileCompressor +{ + void Compress(string inputFilePath, string outputFilePath); + void Inflate(string inputFilePath, string outputFilePath); +} diff --git a/HuffmanCompressor.Lib/Node.cs b/HuffmanCompressor.Lib/Node.cs new file mode 100644 index 0000000..1988637 --- /dev/null +++ b/HuffmanCompressor.Lib/Node.cs @@ -0,0 +1,74 @@ +namespace HuffmanCompressor.Lib; + +/// +/// Represents a node in a binary tree. +/// +/// Data type. +internal class Node +{ + private Node? left; + private Node? right; + private T? value; + + /// + /// Creates a new instance of class. + /// + /// Value of the node + public Node(T value) + { + this.left = null; + this.right = null; + this.value = value; + } + + /// + /// Creates a new instance of class. + /// + /// Left node. + /// Right node. + public Node(Node left, Node right) + { + this.left = left; + this.right = right; + } + + /// + /// Gets the left node. + /// + /// Left node. + public Node? GetLeft() + { + return this.left; + } + + /// + /// Gets the right node. + /// + /// Right node. + public Node? GetRight() + { + return this.right; + } + + /// + /// Gets a value indicating whether the node is a leaf node. + /// + public bool IsLeafNode + { + get + { + return left == null && right == null; + } + } + + /// + /// Gets the value of the node. + /// + public T? Value + { + get + { + return value; + } + } +} diff --git a/HuffmanCompressor/Properties/AssemblyInfo.cs b/HuffmanCompressor.Lib/Properties/AssemblyInfo.cs similarity index 93% rename from HuffmanCompressor/Properties/AssemblyInfo.cs rename to HuffmanCompressor.Lib/Properties/AssemblyInfo.cs index fc243c7..6aaa1e9 100644 --- a/HuffmanCompressor/Properties/AssemblyInfo.cs +++ b/HuffmanCompressor.Lib/Properties/AssemblyInfo.cs @@ -17,5 +17,5 @@ [assembly: Guid("5c38fdf8-890d-4727-aeb6-52085e532832")] -[assembly: InternalsVisibleTo("HuffmanCompressorTests")] +[assembly: InternalsVisibleTo("HuffmanCompressor.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/HuffmanCompressor/Properties/launchSettings.json b/HuffmanCompressor.Lib/Properties/launchSettings.json similarity index 100% rename from HuffmanCompressor/Properties/launchSettings.json rename to HuffmanCompressor.Lib/Properties/launchSettings.json diff --git a/HuffmanCompressorTests/CodeCoverage/coverageReport.zip b/HuffmanCompressor.Tests/CodeCoverage/coverageReport.zip similarity index 100% rename from HuffmanCompressorTests/CodeCoverage/coverageReport.zip rename to HuffmanCompressor.Tests/CodeCoverage/coverageReport.zip diff --git a/HuffmanCompressorTests/CodeCoverage/report.png b/HuffmanCompressor.Tests/CodeCoverage/report.png similarity index 100% rename from HuffmanCompressorTests/CodeCoverage/report.png rename to HuffmanCompressor.Tests/CodeCoverage/report.png diff --git a/HuffmanCompressor.Tests/EndToEndTests.cs b/HuffmanCompressor.Tests/EndToEndTests.cs new file mode 100644 index 0000000..e93f14a --- /dev/null +++ b/HuffmanCompressor.Tests/EndToEndTests.cs @@ -0,0 +1,47 @@ +using HuffmanCompressor.Cmd; +namespace HuffmanCompressor.Tests; + +public class EndToEndTests +{ + [Theory] + [InlineData("E2E-SmallFile.txt", true)] + [InlineData("E2E-EmptyFile.txt", false)] + [InlineData("E2E-SingleCharacter.txt", false)] + [InlineData("E2E-WordFile.docx", false)] + public void CompressTest(string fileName, bool verifySmallerCompressedFileSize) + { + // Arrange + // Note: Use unique output file name to ensure no collision if tests run in parallel. + var inputFilePath = Utilities.GetTestPath(fileName); + var compressedFilePath = Utilities.GetTestPath(fileName + ".huf"); + var inflatedFilePath = Utilities.GetTestPath(inputFilePath + ".inf"); + + // Act + Program.Main(["compress", inputFilePath, compressedFilePath]); + + // Assert + Assert.True(File.Exists(compressedFilePath)); + + // Verify the compression produced a file smaller in size. + // Note: The compressed file includes the overhead of the character frequency table + // this means that there is a threshold for which compression will not be effective. + // Also, files that are already compressed, such as zip files or docx files have high entropy and will not compress further + FileInfo inputFileInfo, compressedFileInfo, inflatedFileInfo; + inputFileInfo = new FileInfo(inputFilePath); + compressedFileInfo = new FileInfo(compressedFilePath); + if (verifySmallerCompressedFileSize) + { + Assert.True(inputFileInfo.Length > compressedFileInfo.Length); + } + + // Act + Program.Main(["inflate", compressedFilePath, inflatedFilePath]); + inflatedFileInfo = new FileInfo(inflatedFilePath); + + // Assert + Assert.Equal(inputFileInfo.Length, inflatedFileInfo.Length); + var originalHash = Utilities.GetFileHash(fileName); + var inflatedHash = Utilities.GetFileHash(inflatedFilePath); + Assert.Equal(originalHash, inflatedHash); + } +} diff --git a/HuffmanCompressor.Tests/FrequencyCounterTests.cs b/HuffmanCompressor.Tests/FrequencyCounterTests.cs new file mode 100644 index 0000000..bf5be3f --- /dev/null +++ b/HuffmanCompressor.Tests/FrequencyCounterTests.cs @@ -0,0 +1,73 @@ +using HuffmanCompressor.Lib; +namespace HuffmanCompressor.Tests; + +public class FrequencyCounterTests +{ + [Fact] + public void AddItemTest() + { + // Arrange + var counter = new FrequencyCounter(); + + // Act + counter.Increment((byte)'a'); + + // Assert + Assert.Equal((UInt32)1, counter.GetFrequency((byte)'a')); + Assert.Equal((UInt32)0, counter.GetFrequency((byte)'b')); + } + + [Fact] + public void SetFrequencyTest() + { + // Arrange + var counter = new FrequencyCounter(); + + // Act + counter.SetFrequency((byte)'a',23); + + // Assert + Assert.Equal((UInt32)23, counter.GetFrequency((byte)'a')); + Assert.Equal((UInt32)0, counter.GetFrequency((byte)'b')); + } + + [Fact] + public void GetEnumeratorTest() + { + // Arrange + var counter = new FrequencyCounter(); + + // Act + counter.SetFrequency((byte)'a', 23); + + // Assert + Assert.Single(counter.GetEnumerator()); + foreach ( var kvp in counter.GetEnumerator()) + { + Assert.Equal((byte)'a', kvp.Key); + Assert.Equal((UInt32)23, kvp.Value); + } + } + + [Fact] + public void IncrementOverflowTest() + { + // Arrange + var counter = new FrequencyCounter(); + + // Act + counter.SetFrequency((byte)'a', UInt32.MaxValue); + + // Assert + Assert.Single(counter.GetEnumerator()); + Assert.Equal(UInt32.MaxValue, counter.GetFrequency((byte)'a')); + + // Act + counter.Increment((byte)'a'); + + // We don't check for a specific value, we allow the counter to use it's own method of rebasing the count, + // just ensure it didn't wrap back to 0 + Assert.True(counter.GetFrequency((byte)'a') > 0); + + } +} diff --git a/HuffmanCompressorTests/HuffmanCompressorTests.csproj b/HuffmanCompressor.Tests/HuffmanCompressor.Tests.csproj similarity index 91% rename from HuffmanCompressorTests/HuffmanCompressorTests.csproj rename to HuffmanCompressor.Tests/HuffmanCompressor.Tests.csproj index 5d81aa9..4bbc4cd 100644 --- a/HuffmanCompressorTests/HuffmanCompressorTests.csproj +++ b/HuffmanCompressor.Tests/HuffmanCompressor.Tests.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/HuffmanCompressor.Tests/HuffmanCompressorTests.cs b/HuffmanCompressor.Tests/HuffmanCompressorTests.cs new file mode 100644 index 0000000..a8fbff5 --- /dev/null +++ b/HuffmanCompressor.Tests/HuffmanCompressorTests.cs @@ -0,0 +1,91 @@ +namespace HuffmanCompressor.Tests; +using HuffmanCompressor.Lib; + +public class HuffmanCompressorTests +{ + private const string TestDataFolderName = "TestData"; + + [Fact] + public void ConstructorTest() + { + // Arrange & Act + var compressor = new HuffmanCompressor(); + + // Assert + Assert.NotNull(compressor); + } + + [Fact] + public void CompressThrowsExceptionForWhitespaceInputFileName() + { + // Arrange + var compressor = new HuffmanCompressor(); + + // Act & Assert + Assert.Throws(() => compressor.Compress(string.Empty, "outputFile.bin")); + } + + [Fact] + public void CompressThrowsExceptionForWhitespaceOutputFileName() + { + // Arrange + var compressor = new HuffmanCompressor(); + + // Act & Assert + Assert.Throws(() => compressor.Compress("inputFile.txt", string.Empty)); + } + + [Fact] + public void InflateThrowsExceptionForWhitespaceInputFileName() + { + // Arrange + var compressor = new HuffmanCompressor(); + + // Act & Assert + Assert.Throws(() => compressor.Inflate(string.Empty, "outputFile.bin")); + } + + [Fact] + public void InflateThrowsExceptionForWhitespaceOutputFileName() + { + // Arrange + var compressor = new HuffmanCompressor(); + + // Act & Assert + Assert.Throws(() => compressor.Inflate("inputFile.huf", string.Empty)); + } + + [Theory] + [InlineData("Smallfile.txt")] + [InlineData("Emptyfile.txt")] + [InlineData("SingleCharacter.txt")] + [InlineData("WordFile.docx")] + public void CompressAndInflateTest(string fileName) + { + // Arrange + var inputFilePath = Utilities.GetTestPath(fileName); + var compressedFilePath = Utilities.GetTestPath(fileName + ".huf"); + var inflatedFilePath = Utilities.GetTestPath(compressedFilePath + ".inf"); + + var compressor = new HuffmanCompressor(); + + // Act + compressor.Compress(inputFilePath, compressedFilePath); + + // Assert + Assert.True(File.Exists(compressedFilePath)); + + // Act + compressor.Inflate(compressedFilePath, inflatedFilePath); + + // Assert + Assert.True(File.Exists(inflatedFilePath)); + var inputFileInfo = new FileInfo(inputFilePath); + var inflatedFileInfo = new FileInfo(inflatedFilePath); + Assert.Equal(inputFileInfo.Length, inflatedFileInfo.Length); + + var originalHash = Utilities.GetFileHash(fileName); + var inflatedHash = Utilities.GetFileHash(inflatedFilePath); + Assert.Equal(originalHash, inflatedHash); + } +} diff --git a/HuffmanCompressor.Tests/ProgramTests.cs b/HuffmanCompressor.Tests/ProgramTests.cs new file mode 100644 index 0000000..ac531eb --- /dev/null +++ b/HuffmanCompressor.Tests/ProgramTests.cs @@ -0,0 +1,69 @@ +namespace HuffmanCompressor.Tests; +using HuffmanCompressor.Cmd; +using HuffmanCompressor.Lib; +using Moq; + +public class ProgramTests +{ + [Fact] + public void CompressMethodInvokedWithCompressParameterTest() + { + // Arrange + var mockCompressor = new Mock(MockBehavior.Strict); + mockCompressor.Setup(c => c.Compress(It.IsAny(), It.IsAny())); + var program = new Program(mockCompressor.Object); + + // Act + program.Run(["compress", "input.txt", "output.bin" ]); + + // Assert + mockCompressor.Verify(compressor => compressor.Compress( + It.Is(s => s.Equals("input.txt")), + It.Is(s => s.Equals("output.bin"))), + "Expected input or output paths not found"); + } + + [Fact] + public void InflateMethodInvokedWithCompressParameterTest() + { + // Arrange + var mockCompressor = new Mock(MockBehavior.Strict); + mockCompressor.Setup(c => c.Inflate(It.IsAny(), It.IsAny())); + var program = new Program(mockCompressor.Object); + + // Act + program.Run(["inflate", "input.bin", "output.txt"]); + + // Assert + mockCompressor.Verify(compressor => compressor.Inflate( + It.Is(s => s.Equals("input.bin")), + It.Is(s => s.Equals("output.txt"))), + "Expected input or output paths not found"); + } + + [Fact] + public void ArgumentExceptionThrownWithInvalidCommandTest() + { + // Arrange + var mockCompressor = new Mock(MockBehavior.Strict); + var program = new Program(mockCompressor.Object); + + // Act & Assert + Assert.Throws( + () => program.Run(["foo", "input.txt", "output.bin"])); + } + + [Theory] + [InlineData("compress")] + [InlineData("foo")] + public void ArgumentExceptionThrownWithInvalidNumberOfParametersTest(string command) + { + // Arrange + var mockCompressor = new Mock(MockBehavior.Strict); + var program = new Program(mockCompressor.Object); + + // Act & Assert + Assert.Throws( + () => program.Run([command])); + } +} \ No newline at end of file diff --git a/HuffmanCompressorTests/TestData/E2E-EmptyFile.txt b/HuffmanCompressor.Tests/TestData/E2E-EmptyFile.txt similarity index 100% rename from HuffmanCompressorTests/TestData/E2E-EmptyFile.txt rename to HuffmanCompressor.Tests/TestData/E2E-EmptyFile.txt diff --git a/HuffmanCompressorTests/TestData/E2E-SingleCharacter.txt b/HuffmanCompressor.Tests/TestData/E2E-SingleCharacter.txt similarity index 100% rename from HuffmanCompressorTests/TestData/E2E-SingleCharacter.txt rename to HuffmanCompressor.Tests/TestData/E2E-SingleCharacter.txt diff --git a/HuffmanCompressorTests/TestData/E2E-SmallFile.txt b/HuffmanCompressor.Tests/TestData/E2E-SmallFile.txt similarity index 100% rename from HuffmanCompressorTests/TestData/E2E-SmallFile.txt rename to HuffmanCompressor.Tests/TestData/E2E-SmallFile.txt diff --git a/HuffmanCompressorTests/TestData/E2E-WordFile.docx b/HuffmanCompressor.Tests/TestData/E2E-WordFile.docx similarity index 100% rename from HuffmanCompressorTests/TestData/E2E-WordFile.docx rename to HuffmanCompressor.Tests/TestData/E2E-WordFile.docx diff --git a/HuffmanCompressorTests/TestData/EmptyFile.txt b/HuffmanCompressor.Tests/TestData/EmptyFile.txt similarity index 100% rename from HuffmanCompressorTests/TestData/EmptyFile.txt rename to HuffmanCompressor.Tests/TestData/EmptyFile.txt diff --git a/HuffmanCompressorTests/TestData/SingleCharacter.txt b/HuffmanCompressor.Tests/TestData/SingleCharacter.txt similarity index 100% rename from HuffmanCompressorTests/TestData/SingleCharacter.txt rename to HuffmanCompressor.Tests/TestData/SingleCharacter.txt diff --git a/HuffmanCompressorTests/TestData/SmallFile.txt b/HuffmanCompressor.Tests/TestData/SmallFile.txt similarity index 100% rename from HuffmanCompressorTests/TestData/SmallFile.txt rename to HuffmanCompressor.Tests/TestData/SmallFile.txt diff --git a/HuffmanCompressorTests/TestData/wordFile.docx b/HuffmanCompressor.Tests/TestData/WordFile.docx similarity index 100% rename from HuffmanCompressorTests/TestData/wordFile.docx rename to HuffmanCompressor.Tests/TestData/WordFile.docx diff --git a/HuffmanCompressor.Tests/Utilities.cs b/HuffmanCompressor.Tests/Utilities.cs new file mode 100644 index 0000000..2762a7a --- /dev/null +++ b/HuffmanCompressor.Tests/Utilities.cs @@ -0,0 +1,27 @@ +namespace HuffmanCompressor.Tests; +using System.IO.Hashing; +using System.Reflection; +using System.Text; + +public static class Utilities +{ + public const string TestDataFolderName = "TestData"; + + public static string GetTestPath(string relativePath) + { + var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().Location); + var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath); + var dirPath = Path.GetDirectoryName(codeBasePath); + return Path.Combine(dirPath!, TestDataFolderName, relativePath); + } + + public static string GetFileHash(string fileName) + { + using (FileStream fileStream = File.OpenRead(GetTestPath(fileName))) + { + var crc32 = new Crc32(); + crc32.Append(fileStream); + return Encoding.UTF8.GetString(crc32.GetCurrentHash()); + } + } +} diff --git a/HuffmanCompressor.sln b/HuffmanCompressor.sln index f15978b..d543e5f 100644 --- a/HuffmanCompressor.sln +++ b/HuffmanCompressor.sln @@ -3,13 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34202.233 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuffmanCompressorLib", "HuffmanCompressor\HuffmanCompressorLib.csproj", "{93160A18-48C3-4AB8-A9C9-2EA2D2C46B01}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuffmanCompressor.Lib", "HuffmanCompressor.Lib\HuffmanCompressor.Lib.csproj", "{93160A18-48C3-4AB8-A9C9-2EA2D2C46B01}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuffmanCompressorTests", "HuffmanCompressorTests\HuffmanCompressorTests.csproj", "{304D0AF6-D701-40E9-8A2F-E2508A12ED5F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HuffmanCompressor.Tests", "HuffmanCompressor.Tests\HuffmanCompressor.Tests.csproj", "{304D0AF6-D701-40E9-8A2F-E2508A12ED5F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HuffmanCompressorWinFormsApp", "HufmannCompressorWinFormsApp\HuffmanCompressorWinFormsApp.csproj", "{78D7EB54-F5CB-45E8-B293-2B95EA522589}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HuffmanCompressor.WinFormsApp", "HufmannCompressor.WinFormsApp\HuffmanCompressor.WinFormsApp.csproj", "{78D7EB54-F5CB-45E8-B293-2B95EA522589}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HuffmanCompressorCmd", "HuffmanCompressorCmd\HuffmanCompressorCmd.csproj", "{567B674B-79D9-4331-A6D6-A13735DA3327}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HuffmanCompressor.Cmd", "HuffmanCompressor.Cmd\HuffmanCompressor.Cmd.csproj", "{567B674B-79D9-4331-A6D6-A13735DA3327}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/HuffmanCompressor/BitReader.cs b/HuffmanCompressor/BitReader.cs deleted file mode 100644 index 6bbdc28..0000000 --- a/HuffmanCompressor/BitReader.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace HuffmanCompressorLib -{ - /// - /// Provides functionality to read individual bits from an input file stream. - /// - internal class BitReader - { - private int bitIndex; - private byte currentByte; - private readonly FileStream fileHandle; - - /// - /// Creates an instance of class. - /// - /// File stream to which the reader will read bits from. - public BitReader(FileStream fileHandle) - { - ArgumentNullException.ThrowIfNull(fileHandle); - this.bitIndex = 8; - this.currentByte = 0x00; - this.fileHandle = fileHandle; - } - - /// - /// Reads a bit from the file. - /// - /// Bit represented as a '0' or '1' character. - /// If the end of the stream is reached while attempting to read. - public char ReadNextBit() - { - // Read 1 byte from the input file stream at a time, and use an index to return each bit from the byte. - // When we get to the last bit, read the next byte and continue until end of file - if (this.bitIndex >= 8) - { - var byteValue = this.fileHandle.ReadByte(); - if (byteValue == -1) - { - throw new EndOfStreamException("Attempting to read past the end of the stream!"); - } - this.currentByte = (byte)byteValue; - this.bitIndex = 0; - } - - byte mask = (byte)(0x80 >> this.bitIndex++); - return (this.currentByte & mask) == 0x00 - ? '0' - : '1'; - } - } -} diff --git a/HuffmanCompressor/BitWriter.cs b/HuffmanCompressor/BitWriter.cs deleted file mode 100644 index bab5562..0000000 --- a/HuffmanCompressor/BitWriter.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace HuffmanCompressorLib -{ - /// - /// Provides functionality to write individual bits to an output file stream. - /// - internal class BitWriter - { - private int bitIndex; - private byte currentByte; - private readonly FileStream fileHandle; - - /// - /// Creates an instance of class. - /// - /// File stream to which the writer will write bits to. - public BitWriter(FileStream fileHandle) - { - ArgumentNullException.ThrowIfNull(fileHandle); - this.bitIndex = 0; - this.currentByte = 0x00; - this.fileHandle = fileHandle; - } - - /// - /// Writes bits to the output file. - /// - /// String of bits, only 0 and 1 are supported. - /// In case the bit string contains unsupported characters. - public void WriteBits(string bitString) - { - ArgumentNullException.ThrowIfNull(bitString); - - // Fill in currentByte from most significant bit to least significant bit by reading from input bitString - // until the input bitString is exhausted. - // As we fill out all available bits in currentByte, we flush it out to the file and start over with a fresh byte - for (int i = 0; i < bitString.Length; i++) - { - var bit = bitString[i]; - switch(bit) - { - case '0': - break; - case '1': - // mask a '1' on the current byte - byte mask = 0x80; - mask = (byte)(mask >> bitIndex); - currentByte |= mask; - break; - default: - throw new ArgumentException($"Invalid bit detected {bit}"); - } - - if (bitIndex == 7) - { - // currentByte has been exhausted for all of its 8 available bits - // commit to disk and mint a new byte to continue the process. - fileHandle.WriteByte(currentByte); - bitIndex = 0; - currentByte = 0x00; - } - else - { - bitIndex++; - } - } - } - - /// - /// Write the outstanding byte to disk, prior to closing the file handle. - /// The remaining bits will be padding. - /// - public void Flush() - { - fileHandle.WriteByte(currentByte); - } - } -} diff --git a/HuffmanCompressor/FrequencyCounter.cs b/HuffmanCompressor/FrequencyCounter.cs deleted file mode 100644 index 919b5f0..0000000 --- a/HuffmanCompressor/FrequencyCounter.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace HuffmanCompressorLib -{ - /// - /// Class to count the relative frequency of each byte in a stream of data. - /// There is no upper bound on the number of bytes that can be counted. - /// If the counter reaches the maximum value, the class will rebase all of the frequencies to avoid overflows. - /// - internal class FrequencyCounter - { - private readonly IDictionary moduloCounter; - private readonly IDictionary frequencies; - private int multiplier; - - /// - /// Initializes a new instance of the class. - /// - public FrequencyCounter() - { - this.moduloCounter = new Dictionary(); - this.frequencies = new Dictionary(); - multiplier = 1; - - // Initialize the dictionaries to make the code more straightforward in the rest of the class. - // Avoid overflow of counter that would lead to an infinite loop using an int - // https://stackoverflow.com/questions/43800147/iterating-from-minvalue-to-maxvalue-with-overflow - for (int i = byte.MinValue; i <= byte.MaxValue; i++) - { - this.moduloCounter.Add((byte)i, 0); - this.frequencies.Add((byte)i, 0); - } - } - - /// - /// Increment the frequency of a byte by 1. - /// - /// The value to increment the frequency of. - public void Increment(byte value) - { - // Check to see if we have reached the rare theoretic limit for frequency counting. - // In that case, the best we can do is stop counting to avoid an overlfow back to 0. - // Note: this would still mean having read 16 million TB of a single character! - if (this.moduloCounter[value] == UInt32.MaxValue && - this.frequencies[value] == UInt32.MaxValue) - { - return; - } - - this.moduloCounter[value]++; - if (this.moduloCounter[value] == multiplier) - { - // If the counter is about to overflow, we call the Rebase function - if (this.frequencies[value] == UInt32.MaxValue) - { - this.Rebase(); - } - else - { - this.frequencies[value]++; - } - - this.moduloCounter[value] = 0; - } - } - - /// - /// Set the frequency of a byte to a specific value. - /// - /// The value to set. - /// The frequency to set. - public void SetFrequency(byte value, UInt32 frequency) - { - this.frequencies[(byte)value] = frequency; - this.moduloCounter[(byte)value] = 0; - } - - /// - /// Get the frequency of a byte. - /// - /// The value to query the frequency of - /// Frequency of the value. - public UInt32 GetFrequency(byte value) - { - // First check for the case that the frequency counter has not yet been - // bumped up, in that case, if we ever counted the value in the modulo counter, we must return - // the smallest value possible, which is 1. - if (this.frequencies[value] == 0) - { - if (this.moduloCounter[value] > 0) - { - return 1; - } - - return 0; - } - - return this.frequencies[value]; - } - - /// - /// Enumerator to allow caller to cycle through frequencies. - /// - /// Key value pair enumaration of non-zero frequencies - public IEnumerable> GetEnumerator() - { - for (int b = byte.MinValue; b <= byte.MaxValue; b++) - { - var frequency = this.GetFrequency((byte)b); - if (frequency > 0) - { - yield return new KeyValuePair((byte)b, frequency); - } - } - } - - /// - /// Rebases all of the frequencies uniformly by dividing them by 2 - /// This allows us to handle very high counts without risk of overflowing and still - /// preserving statistical accuracy. - /// - private void Rebase() - { - this.frequencies.ToList().ForEach( kvp => this.frequencies[kvp.Key] /= 2 + 1); - this.multiplier *= 2; - } - - } -} diff --git a/HuffmanCompressor/HuffmanCompressor.cs b/HuffmanCompressor/HuffmanCompressor.cs deleted file mode 100644 index be80460..0000000 --- a/HuffmanCompressor/HuffmanCompressor.cs +++ /dev/null @@ -1,252 +0,0 @@ -namespace HuffmanCompressorLib -{ - using System; - using System.Collections.Generic; - using System.Linq; - - /// - /// Class that implements the Huffman compression algorithm. - /// - public class HuffmanCompressor : IFileCompressor - { - private FrequencyCounter frequencyCounter; - private IDictionary binaryCodeMappings; - private Node? treeRoot; - - /// - /// Code to mark End of File character. Must not be a confused with a regular byte 0 to 255. - /// - private const short EndOfFileCode = -1; - - /// - /// Initializes a new instance of the class. - /// - public HuffmanCompressor() - { - this.frequencyCounter = new FrequencyCounter(); - this.binaryCodeMappings = new Dictionary(); - } - - /// - /// Compresses a file. - /// - /// File to be compressed. - /// Path of destination (compressed) file. - /// is . - /// is . - - public void Compress(string inputFilePath, string outputFilePath) - { - ArgumentNullException.ThrowIfNullOrWhiteSpace(inputFilePath); - ArgumentNullException.ThrowIfNullOrWhiteSpace(outputFilePath); - - this.InitializeFrequencyDictionary(inputFilePath); - this.BuildTree(); - this.BuildBinaryCodeMappings(); - this.CompressInternal(inputFilePath, outputFilePath); - } - - /// - /// Inflates a file. - /// - /// File to be inflated. - /// Path of destination (inflated) file. - /// is . - /// is . - - public void Inflate(string inputFilePath, string outputFilePath) - { - ArgumentNullException.ThrowIfNullOrWhiteSpace(inputFilePath); - ArgumentNullException.ThrowIfNullOrWhiteSpace(outputFilePath); - - var inputStream = this.ReadFrequencyDictionary(inputFilePath); - this.BuildTree(); - this.BuildBinaryCodeMappings(); - this.InflateInternal(inputStream, outputFilePath); - } - - private void InitializeFrequencyDictionary(string inputFilePath) - { - this.frequencyCounter = new FrequencyCounter(); - using (var inputStream = File.OpenRead(inputFilePath)) - { - int inputByte; - // -1 represents end of stream, otherwise byte cast as int - while ((inputByte = inputStream.ReadByte()) != -1) - { - byte nativeByte = (byte)inputByte; - this.frequencyCounter.Increment(nativeByte); - } - } - } - - private void BuildTree() - { - // Use a MinHeap priority queue, creating a node with the byte, and the frequency as priority - var priorityQueue = new PriorityQueue, UInt32>(); - foreach (var kvp in frequencyCounter.GetEnumerator()) - { - priorityQueue.Enqueue(new Node(kvp.Key), kvp.Value); - } - - // Enque special "end of file" node with value -1 and priority 0. This special marker will appear at the end of the file so that during - // decompression we deterministically know we have exhausted the input file. This allows us to compress arbitrarily large input files without having to save in - // the file header the number of bytes in the input, which could overflow for very large files. - priorityQueue.Enqueue(new Node(EndOfFileCode), 0); - - // Take the 2 nodes at the head of the queue (lowest frequency) and combine into a new node - // The tree is complete when there is only 1 node left - while (priorityQueue.Count > 1) - { - priorityQueue.TryDequeue(out var leftNode, out UInt32 leftPriority); - priorityQueue.TryDequeue(out var rightNode, out UInt32 rightPriority); - priorityQueue.Enqueue(new Node(leftNode!, rightNode!), leftPriority + rightPriority); - } - - this.treeRoot = priorityQueue.Dequeue(); - } - - private void BuildBinaryCodeMappings() - { - this.binaryCodeMappings = new Dictionary(); - BuildBinaryCodeMappings(this.treeRoot!, string.Empty); - } - - private void BuildBinaryCodeMappings(Node node, string binaryCode) - { - if (node.IsLeafNode) - { - this.binaryCodeMappings!.Add(node.Value, binaryCode); - return; - } - - // Left node gets tagged with 0, right node gets tagged with 1 - this.BuildBinaryCodeMappings(node.GetLeft()!, $"{binaryCode}0"); - this.BuildBinaryCodeMappings(node.GetRight()!, $"{binaryCode}1"); - } - - private void CompressInternal(string inputFilePath, string outputFilePath) - { - using (var inputStream = File.OpenRead(inputFilePath)) - { - // Open a filestream to the destination (compressed) file. If the file already exists, it will be overwritten. - using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) - { - this.WriteFrequencyDictionary(outputStream); - - var bitWriter = new BitWriter(outputStream); - - // Main loop to encode each byte in the input stream according to its binary code mapping - int inputByte; - while ((inputByte = inputStream.ReadByte()) != -1) - { - bitWriter.WriteBits(this.binaryCodeMappings![(byte)inputByte]); - } - - // Write End of file code at the very end. - bitWriter.WriteBits(this.binaryCodeMappings![EndOfFileCode]); - - bitWriter.Flush(); - } - } - } - - /// - /// Writes the frequency dictionary to the output file so that the binary tree can be rebuilt to decompress the file. - /// As an optimization, we only write frequencies for the bytes that were present in the input file. - /// Note: storing the binary code mappings could be used as an alternate way to decode the file, but it would consume more disk space than the frequencies table - /// defeating the purpose of an efficient compression algorithm. Instead, the binary code mappings will be rebuilt at time of decompression from this frequency table. - /// - /// - private void WriteFrequencyDictionary(FileStream outputStream) - { - var writer = new BinaryWriter(outputStream); - - // frequency table size can be 256 + 1 end of file character (257) at most, so we need 2 bytes at most (ushort) on the header of size of frequency table - writer.Write((ushort)this.frequencyCounter.GetEnumerator().Count()); - foreach (var kvp in this.frequencyCounter.GetEnumerator()) - { - writer.Write(kvp.Key); - writer.Write(kvp.Value); - } - writer.Flush(); - } - - private FileStream ReadFrequencyDictionary(string inputFilePath) - { - this.frequencyCounter = new FrequencyCounter(); - FileStream inputStream; - try - { - inputStream = File.OpenRead(inputFilePath); - } - catch (Exception e) - { - Console.WriteLine($"Exception opening input file: {e.Message}"); - throw; - } - - var reader = new BinaryReader(inputStream); - var frequencyTableSize = reader.ReadUInt16(); - // 256 = all possible 8 bit characters - if (frequencyTableSize < 0 || frequencyTableSize > 256) - { - throw new ArgumentOutOfRangeException($"Invalid frequency count {frequencyTableSize} in input file"); - } - - for (int i = 0; i < frequencyTableSize; i++) - { - var key = reader.ReadByte(); - var value = reader.ReadUInt32(); - this.frequencyCounter.SetFrequency(key, value); - } - - // Hand off the input stream to the next steps of decompression so they continue reading after the frequency dictionary header - return inputStream; - } - - private void InflateInternal(FileStream inputStream, string outputFilePath) - { - // Open a filestream to the destination (uncompressed) file. If the file already exists, it will be overwritten. - using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) - { - // Special case if input file was empty, nothing to do - if (this.frequencyCounter.GetEnumerator().Count() == 0) - { - return; - } - - // For decompression, we key off binary codes to obtain the corresponding byte, which is the opposite to what we do in compression. - // This ensures that lookup for decompression has constant time complexity. - var reverseBinaryCodeMappings = this.binaryCodeMappings?.ToDictionary(kvp => kvp.Value, kvp => kvp.Key); - var bitReader = new BitReader(inputStream); - var endOfFileFound = false; - while (!endOfFileFound) - { - var bitString = string.Empty; - var binaryCodeMatch = false; - while (!binaryCodeMatch) - { - var bit = bitReader.ReadNextBit(); - bitString = $"{bitString}{bit}"; - if (reverseBinaryCodeMappings!.ContainsKey(bitString)) - { - binaryCodeMatch = true; - if (reverseBinaryCodeMappings![bitString] == EndOfFileCode) - { - endOfFileFound = true; - } - else - { - // A matching pattern in the binary code mappings is found, write the corresponding byte to the output - outputStream.WriteByte((byte)reverseBinaryCodeMappings[bitString]); - binaryCodeMatch = true; - } - } - } - } - inputStream.Close(); - } - } - } -} \ No newline at end of file diff --git a/HuffmanCompressor/IFileCompressor.cs b/HuffmanCompressor/IFileCompressor.cs deleted file mode 100644 index d77d10d..0000000 --- a/HuffmanCompressor/IFileCompressor.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace HuffmanCompressorLib -{ - /// - /// Interface for file compression and decompression. - /// - public interface IFileCompressor - { - void Compress(string inputFilePath, string outputFilePath); - void Inflate(string inputFilePath, string outputFilePath); - } -} diff --git a/HuffmanCompressor/Node.cs b/HuffmanCompressor/Node.cs deleted file mode 100644 index 86850cc..0000000 --- a/HuffmanCompressor/Node.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace HuffmanCompressorLib -{ - /// - /// Represents a node in a binary tree. - /// - /// Data type. - internal class Node - { - private Node? left; - private Node? right; - private T? value; - - /// - /// Creates a new instance of class. - /// - /// Value of the node - public Node(T value) - { - this.left = null; - this.right = null; - this.value = value; - } - - /// - /// Creates a new instance of class. - /// - /// Left node. - /// Right node. - public Node(Node left, Node right) - { - this.left = left; - this.right = right; - } - - /// - /// Gets the left node. - /// - /// Left node. - public Node? GetLeft() - { - return this.left; - } - - /// - /// Gets the right node. - /// - /// Right node. - public Node? GetRight() - { - return this.right; - } - - /// - /// Gets a value indicating whether the node is a leaf node. - /// - public bool IsLeafNode - { - get - { - return left == null && right == null; - } - } - - /// - /// Gets the value of the node. - /// - public T? Value - { - get - { - return value; - } - } - } -} diff --git a/HuffmanCompressorCmd/Program.cs b/HuffmanCompressorCmd/Program.cs deleted file mode 100644 index 1f8101e..0000000 --- a/HuffmanCompressorCmd/Program.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace HuffmanCompressorCmd -{ - using HuffmanCompressorLib; - - /// - /// Main entry point for the HuffmanCompressorCmd console application. - /// - public class Program - { - private IFileCompressor _compressor; - - /// - /// Constructor of the Program class. - /// - public Program() - { - _compressor = new HuffmanCompressor(); - } - - /// - /// Internal method intended for the unit tests to be able to inject a mock interface for testing purposes. - /// - /// Instance of the compressor interface. - internal void SetCompressorReference(IFileCompressor compressor) - { - _compressor = compressor; - } - - /// - /// The main entry point (Main method) for the application always needs to be declared as static. - /// That means that it cannot access non-static members of the class, such as _compressor. - /// Declaring the _compressor member as static would create a single instance of the compressor for all instances of the Program class. - /// This would be a problem if the Program class was used in a multi-threaded environment, such as when unit tests are run, where we - /// may run parallel compression jobs or even want to inject a mock compressor for testing purposes. - /// To avoid this, we create the Run wrapper as non-static and have Main create an instance of the Program class to invoke it. - /// - /// Program args - /// - public void Run(string[] args) - { - const string usageString = "Usage: [compress|inflate] [input file path] [output file path]"; - - if (args.Length != 3) - { - Console.WriteLine(usageString); - throw new ArgumentException(usageString); - } - - switch (args[0].ToLower()) - { - case "compress": - _compressor.Compress(args[1], args[2]); - break; - case "inflate": - _compressor.Inflate(args[1], args[2]); - break; - default: - Console.WriteLine(usageString); - throw new ArgumentException(usageString); - } - } - - /// - /// Main entry point for the HuffmanCompressorCmd console application. - /// - /// Program arguments. - public static void Main(string[] args) - { - var program = new Program(); - program.Run(args); - } - } -} \ No newline at end of file diff --git a/HuffmanCompressorTests/EndToEndTests.cs b/HuffmanCompressorTests/EndToEndTests.cs deleted file mode 100644 index bb31a37..0000000 --- a/HuffmanCompressorTests/EndToEndTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace HuffmanCompressorTests -{ - using HuffmanCompressorCmd; - - public class EndToEndTests - { - [Theory] - [InlineData("E2E-SmallFile.txt", true)] - [InlineData("E2E-EmptyFile.txt", false)] - [InlineData("E2E-SingleCharacter.txt", false)] - [InlineData("E2E-WordFile.docx", false)] - public void CompressTest(string fileName, bool verifySmallerCompressedFileSize) - { - // Arrange - // Note: Use unique output file name to ensure no collision if tests run in parallel. - var inputFilePath = Utilities.GetTestPath(fileName); - var compressedFilePath = Utilities.GetTestPath(fileName + ".huf"); - var inflatedFilePath = Utilities.GetTestPath(inputFilePath + ".inf"); - - // Act - Program.Main(["compress", inputFilePath, compressedFilePath]); - - // Assert - Assert.True(File.Exists(compressedFilePath)); - - // Verify the compression produced a file smaller in size. - // Note: The compressed file includes the overhead of the character frequency table - // this means that there is a threshold for which compression will not be effective. - // Also, files that are already compressed, such as zip files or docx files have high entropy and will not compress further - FileInfo inputFileInfo, compressedFileInfo, inflatedFileInfo; - inputFileInfo = new FileInfo(inputFilePath); - compressedFileInfo = new FileInfo(compressedFilePath); - if (verifySmallerCompressedFileSize) - { - Assert.True(inputFileInfo.Length > compressedFileInfo.Length); - } - - // Act - Program.Main(["inflate", compressedFilePath, inflatedFilePath]); - inflatedFileInfo = new FileInfo(inflatedFilePath); - - // Assert - Assert.Equal(inputFileInfo.Length, inflatedFileInfo.Length); - var originalHash = Utilities.GetFileHash(fileName); - var inflatedHash = Utilities.GetFileHash(inflatedFilePath); - Assert.Equal(originalHash, inflatedHash); - } - } -} diff --git a/HuffmanCompressorTests/FrequencyCounterTests.cs b/HuffmanCompressorTests/FrequencyCounterTests.cs deleted file mode 100644 index 02d9736..0000000 --- a/HuffmanCompressorTests/FrequencyCounterTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace HuffmanCompressorTests -{ - using HuffmanCompressorLib; - - public class FrequencyCounterTests - { - [Fact] - public void AddItemTest() - { - // Arrange - var counter = new FrequencyCounter(); - - // Act - counter.Increment((byte)'a'); - - // Assert - Assert.Equal((UInt32)1, counter.GetFrequency((byte)'a')); - Assert.Equal((UInt32)0, counter.GetFrequency((byte)'b')); - } - - [Fact] - public void SetFrequencyTest() - { - // Arrange - var counter = new FrequencyCounter(); - - // Act - counter.SetFrequency((byte)'a',23); - - // Assert - Assert.Equal((UInt32)23, counter.GetFrequency((byte)'a')); - Assert.Equal((UInt32)0, counter.GetFrequency((byte)'b')); - } - - [Fact] - public void GetEnumeratorTest() - { - // Arrange - var counter = new FrequencyCounter(); - - // Act - counter.SetFrequency((byte)'a', 23); - - // Assert - Assert.Single(counter.GetEnumerator()); - foreach ( var kvp in counter.GetEnumerator()) - { - Assert.Equal((byte)'a', kvp.Key); - Assert.Equal((UInt32)23, kvp.Value); - } - } - - [Fact] - public void IncrementOverflowTest() - { - // Arrange - var counter = new FrequencyCounter(); - - // Act - counter.SetFrequency((byte)'a', UInt32.MaxValue); - - // Assert - Assert.Single(counter.GetEnumerator()); - Assert.Equal(UInt32.MaxValue, counter.GetFrequency((byte)'a')); - - // Act - counter.Increment((byte)'a'); - - // We don't check for a specific value, we allow the counter to use it's own method of rebasing the count, - // just ensure it didn't wrap back to 0 - Assert.True(counter.GetFrequency((byte)'a') > 0); - - } - } -} diff --git a/HuffmanCompressorTests/HuffmanCompressorTests.cs b/HuffmanCompressorTests/HuffmanCompressorTests.cs deleted file mode 100644 index ecc36b0..0000000 --- a/HuffmanCompressorTests/HuffmanCompressorTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace HuffmanCompressorTests -{ - using HuffmanCompressorLib; - - public class HuffmanCompressorTests - { - private const string TestDataFolderName = "TestData"; - - [Fact] - public void ConstructorTest() - { - // Arrange & Act - var compressor = new HuffmanCompressor(); - - // Assert - Assert.NotNull(compressor); - } - - [Fact] - public void CompressThrowsExceptionForWhitespaceInputFileName() - { - // Arrange - var compressor = new HuffmanCompressor(); - - // Act & Assert - Assert.Throws(() => compressor.Compress(string.Empty, "outputFile.bin")); - } - - [Fact] - public void CompressThrowsExceptionForWhitespaceOutputFileName() - { - // Arrange - var compressor = new HuffmanCompressor(); - - // Act & Assert - Assert.Throws(() => compressor.Compress("inputFile.txt", string.Empty)); - } - - [Fact] - public void InflateThrowsExceptionForWhitespaceInputFileName() - { - // Arrange - var compressor = new HuffmanCompressor(); - - // Act & Assert - Assert.Throws(() => compressor.Inflate(string.Empty, "outputFile.bin")); - } - - [Fact] - public void InflateThrowsExceptionForWhitespaceOutputFileName() - { - // Arrange - var compressor = new HuffmanCompressor(); - - // Act & Assert - Assert.Throws(() => compressor.Inflate("inputFile.huf", string.Empty)); - } - - [Theory] - [InlineData("Smallfile.txt")] - [InlineData("Emptyfile.txt")] - [InlineData("SingleCharacter.txt")] - [InlineData("WordFile.docx")] - public void CompressAndInflateTest(string fileName) - { - // Arrange - var inputFilePath = Utilities.GetTestPath(fileName); - var compressedFilePath = Utilities.GetTestPath(fileName + ".huf"); - var inflatedFilePath = Utilities.GetTestPath(compressedFilePath + ".inf"); - - var compressor = new HuffmanCompressor(); - - // Act - compressor.Compress(inputFilePath, compressedFilePath); - - // Assert - Assert.True(File.Exists(compressedFilePath)); - - // Act - compressor.Inflate(compressedFilePath, inflatedFilePath); - - // Assert - Assert.True(File.Exists(inflatedFilePath)); - var inputFileInfo = new FileInfo(inputFilePath); - var inflatedFileInfo = new FileInfo(inflatedFilePath); - Assert.Equal(inputFileInfo.Length, inflatedFileInfo.Length); - - var originalHash = Utilities.GetFileHash(fileName); - var inflatedHash = Utilities.GetFileHash(inflatedFilePath); - Assert.Equal(originalHash, inflatedHash); - } - } -} diff --git a/HuffmanCompressorTests/ProgramTests.cs b/HuffmanCompressorTests/ProgramTests.cs deleted file mode 100644 index 009b2df..0000000 --- a/HuffmanCompressorTests/ProgramTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace HuffmanCompressorTests -{ - using HuffmanCompressorCmd; - using HuffmanCompressorLib; - using Moq; - - public class ProgramTests - { - [Fact] - public void CompressMethodInvokedWithCompressParameterTest() - { - // Arrange - var mockCompressor = new Mock(MockBehavior.Strict); - mockCompressor.Setup(c => c.Compress(It.IsAny(), It.IsAny())); - var program = new Program(); - program.SetCompressorReference(mockCompressor.Object); - - // Act - program.Run(["compress", "input.txt", "output.bin" ]); - - // Assert - mockCompressor.Verify(compressor => compressor.Compress( - It.Is(s => s.Equals("input.txt")), - It.Is(s => s.Equals("output.bin"))), - "Expected input or output paths not found"); - } - - [Fact] - public void InflateMethodInvokedWithCompressParameterTest() - { - // Arrange - var mockCompressor = new Mock(MockBehavior.Strict); - mockCompressor.Setup(c => c.Inflate(It.IsAny(), It.IsAny())); - var program = new Program(); - program.SetCompressorReference(mockCompressor.Object); - - // Act - program.Run(["inflate", "input.bin", "output.txt"]); - - // Assert - mockCompressor.Verify(compressor => compressor.Inflate( - It.Is(s => s.Equals("input.bin")), - It.Is(s => s.Equals("output.txt"))), - "Expected input or output paths not found"); - } - - [Fact] - public void ArgumentExceptionThrownWithInvalidCommandTest() - { - // Arrange - var mockCompressor = new Mock(MockBehavior.Strict); - var program = new Program(); - program.SetCompressorReference(mockCompressor.Object); - - // Act & Assert - Assert.Throws( - () => program.Run(["foo", "input.txt", "output.bin"])); - } - - [Theory] - [InlineData("compress")] - [InlineData("foo")] - public void ArgumentExceptionThrownWithInvalidNumberOfParametersTest(string command) - { - // Arrange - var mockCompressor = new Mock(MockBehavior.Strict); - var program = new Program(); - program.SetCompressorReference(mockCompressor.Object); - - // Act & Assert - Assert.Throws( - () => program.Run([command])); - } - } -} \ No newline at end of file diff --git a/HuffmanCompressorTests/Utilities.cs b/HuffmanCompressorTests/Utilities.cs deleted file mode 100644 index 710e282..0000000 --- a/HuffmanCompressorTests/Utilities.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace HuffmanCompressorTests -{ - using System.IO.Hashing; - using System.Reflection; - using System.Text; - - public static class Utilities - { - public const string TestDataFolderName = "TestData"; - - public static string GetTestPath(string relativePath) - { - var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().Location); - var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath); - var dirPath = Path.GetDirectoryName(codeBasePath); - return Path.Combine(dirPath!, TestDataFolderName, relativePath); - } - - public static string GetFileHash(string fileName) - { - using (FileStream fileStream = File.OpenRead(GetTestPath(fileName))) - { - var crc32 = new Crc32(); - crc32.Append(fileStream); - return Encoding.UTF8.GetString(crc32.GetCurrentHash()); - } - } - } -} diff --git a/HufmannCompressorWinFormsApp/HuffmanCompressorWinFormsApp.csproj b/HufmannCompressor.WinFormsApp/HuffmanCompressor.WinFormsApp.csproj similarity index 78% rename from HufmannCompressorWinFormsApp/HuffmanCompressorWinFormsApp.csproj rename to HufmannCompressor.WinFormsApp/HuffmanCompressor.WinFormsApp.csproj index 0dc5b33..403991f 100644 --- a/HufmannCompressorWinFormsApp/HuffmanCompressorWinFormsApp.csproj +++ b/HufmannCompressor.WinFormsApp/HuffmanCompressor.WinFormsApp.csproj @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/HufmannCompressor.WinFormsApp/MainForm.Designer.cs b/HufmannCompressor.WinFormsApp/MainForm.Designer.cs new file mode 100644 index 0000000..2657bf4 --- /dev/null +++ b/HufmannCompressor.WinFormsApp/MainForm.Designer.cs @@ -0,0 +1,108 @@ +namespace HufmannCompressor.WinFormsApp; +partial class MainForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + btnCompressFile = new Button(); + openFileDialog = new OpenFileDialog(); + statusStrip = new StatusStrip(); + toolStripStatusLabel = new ToolStripStatusLabel(); + toolStripProgressBar1 = new ToolStripProgressBar(); + btnInflateFile = new Button(); + statusStrip.SuspendLayout(); + SuspendLayout(); + // + // btnCompressFile + // + btnCompressFile.Location = new Point(38, 44); + btnCompressFile.Margin = new Padding(2); + btnCompressFile.Name = "btnCompressFile"; + btnCompressFile.Size = new Size(152, 36); + btnCompressFile.TabIndex = 0; + btnCompressFile.Text = "Compress file"; + btnCompressFile.UseVisualStyleBackColor = true; + btnCompressFile.Click += btnCompressFile_Click; + // + // statusStrip + // + statusStrip.ImageScalingSize = new Size(24, 24); + statusStrip.Items.AddRange(new ToolStripItem[] { toolStripStatusLabel, toolStripProgressBar1 }); + statusStrip.Location = new Point(0, 320); + statusStrip.Name = "statusStrip"; + statusStrip.Size = new Size(615, 32); + statusStrip.TabIndex = 1; + statusStrip.Text = "statusStrip"; + // + // toolStripStatusLabel + // + toolStripStatusLabel.Name = "toolStripStatusLabel"; + toolStripStatusLabel.Size = new Size(60, 25); + toolStripStatusLabel.Text = "Ready"; + // + // toolStripProgressBar1 + // + toolStripProgressBar1.Name = "toolStripProgressBar1"; + toolStripProgressBar1.Size = new Size(200, 24); + // + // btnInflateFile + // + btnInflateFile.Location = new Point(222, 44); + btnInflateFile.Margin = new Padding(2); + btnInflateFile.Name = "btnInflateFile"; + btnInflateFile.Size = new Size(152, 36); + btnInflateFile.TabIndex = 2; + btnInflateFile.Text = "Inflate file"; + btnInflateFile.UseVisualStyleBackColor = true; + btnInflateFile.Click += btnInflateFile_Click; + // + // MainForm + // + AutoScaleDimensions = new SizeF(10F, 25F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(615, 352); + Controls.Add(btnInflateFile); + Controls.Add(statusStrip); + Controls.Add(btnCompressFile); + Margin = new Padding(2); + Name = "MainForm"; + Text = "Huffman Compressor"; + statusStrip.ResumeLayout(false); + statusStrip.PerformLayout(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Button btnCompressFile; + private OpenFileDialog openFileDialog; + private StatusStrip statusStrip; + private ToolStripStatusLabel toolStripStatusLabel; + private ToolStripProgressBar toolStripProgressBar1; + private Button btnInflateFile; +} + diff --git a/HufmannCompressor.WinFormsApp/MainForm.cs b/HufmannCompressor.WinFormsApp/MainForm.cs new file mode 100644 index 0000000..69ada1d --- /dev/null +++ b/HufmannCompressor.WinFormsApp/MainForm.cs @@ -0,0 +1,41 @@ +namespace HufmannCompressor.WinFormsApp; +using HuffmanCompressor.Lib; + +public partial class MainForm : Form +{ + public MainForm() + { + InitializeComponent(); + } + + private void btnCompressFile_Click(object sender, EventArgs e) + { + openFileDialog.Filter = "All files (*.*)|*.*"; + var openFileDialogResult = openFileDialog.ShowDialog(this); + if (openFileDialogResult != DialogResult.OK) + { + return; + } + + var compressor = new HuffmanCompressor(); + toolStripStatusLabel.Text = "Compressing..."; + compressor.Compress(openFileDialog.FileName, openFileDialog.FileName + ".huf"); + toolStripStatusLabel.Text = "Ready"; + } + + private void btnInflateFile_Click(object sender, EventArgs e) + { + openFileDialog.Filter = "Compressed files (*.huf)|*.huf"; + openFileDialog.AddExtension = true; + var openFileDialogResult = openFileDialog.ShowDialog(this); + if (openFileDialogResult != DialogResult.OK) + { + return; + } + + var compressor = new HuffmanCompressor(); + toolStripStatusLabel.Text = "Inflating..."; + compressor.Inflate(openFileDialog.FileName, openFileDialog.FileName + ".inflated"); + toolStripStatusLabel.Text = "Ready"; + } +} \ No newline at end of file diff --git a/HufmannCompressorWinFormsApp/MainForm.resx b/HufmannCompressor.WinFormsApp/MainForm.resx similarity index 100% rename from HufmannCompressorWinFormsApp/MainForm.resx rename to HufmannCompressor.WinFormsApp/MainForm.resx diff --git a/HufmannCompressorWinFormsApp/Program.cs b/HufmannCompressor.WinFormsApp/Program.cs similarity index 92% rename from HufmannCompressorWinFormsApp/Program.cs rename to HufmannCompressor.WinFormsApp/Program.cs index fdfa4e8..f43e41b 100644 --- a/HufmannCompressorWinFormsApp/Program.cs +++ b/HufmannCompressor.WinFormsApp/Program.cs @@ -1,4 +1,4 @@ -namespace HufmannCompressorWinFormsApp +namespace HufmannCompressor.WinFormsApp { internal static class Program { diff --git a/HufmannCompressorWinFormsApp/MainForm.Designer.cs b/HufmannCompressorWinFormsApp/MainForm.Designer.cs deleted file mode 100644 index c7a810e..0000000 --- a/HufmannCompressorWinFormsApp/MainForm.Designer.cs +++ /dev/null @@ -1,110 +0,0 @@ -namespace HufmannCompressorWinFormsApp -{ - partial class MainForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - btnCompressFile = new Button(); - openFileDialog = new OpenFileDialog(); - statusStrip = new StatusStrip(); - toolStripStatusLabel = new ToolStripStatusLabel(); - toolStripProgressBar1 = new ToolStripProgressBar(); - btnInflateFile = new Button(); - statusStrip.SuspendLayout(); - SuspendLayout(); - // - // btnCompressFile - // - btnCompressFile.Location = new Point(38, 44); - btnCompressFile.Margin = new Padding(2); - btnCompressFile.Name = "btnCompressFile"; - btnCompressFile.Size = new Size(152, 36); - btnCompressFile.TabIndex = 0; - btnCompressFile.Text = "Compress file"; - btnCompressFile.UseVisualStyleBackColor = true; - btnCompressFile.Click += btnCompressFile_Click; - // - // statusStrip - // - statusStrip.ImageScalingSize = new Size(24, 24); - statusStrip.Items.AddRange(new ToolStripItem[] { toolStripStatusLabel, toolStripProgressBar1 }); - statusStrip.Location = new Point(0, 320); - statusStrip.Name = "statusStrip"; - statusStrip.Size = new Size(615, 32); - statusStrip.TabIndex = 1; - statusStrip.Text = "statusStrip"; - // - // toolStripStatusLabel - // - toolStripStatusLabel.Name = "toolStripStatusLabel"; - toolStripStatusLabel.Size = new Size(60, 25); - toolStripStatusLabel.Text = "Ready"; - // - // toolStripProgressBar1 - // - toolStripProgressBar1.Name = "toolStripProgressBar1"; - toolStripProgressBar1.Size = new Size(200, 24); - // - // btnInflateFile - // - btnInflateFile.Location = new Point(222, 44); - btnInflateFile.Margin = new Padding(2); - btnInflateFile.Name = "btnInflateFile"; - btnInflateFile.Size = new Size(152, 36); - btnInflateFile.TabIndex = 2; - btnInflateFile.Text = "Inflate file"; - btnInflateFile.UseVisualStyleBackColor = true; - btnInflateFile.Click += btnInflateFile_Click; - // - // MainForm - // - AutoScaleDimensions = new SizeF(10F, 25F); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(615, 352); - Controls.Add(btnInflateFile); - Controls.Add(statusStrip); - Controls.Add(btnCompressFile); - Margin = new Padding(2); - Name = "MainForm"; - Text = "Huffman Compressor"; - statusStrip.ResumeLayout(false); - statusStrip.PerformLayout(); - ResumeLayout(false); - PerformLayout(); - } - - #endregion - - private Button btnCompressFile; - private OpenFileDialog openFileDialog; - private StatusStrip statusStrip; - private ToolStripStatusLabel toolStripStatusLabel; - private ToolStripProgressBar toolStripProgressBar1; - private Button btnInflateFile; - } -} - diff --git a/HufmannCompressorWinFormsApp/MainForm.cs b/HufmannCompressorWinFormsApp/MainForm.cs deleted file mode 100644 index 3f5329b..0000000 --- a/HufmannCompressorWinFormsApp/MainForm.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace HufmannCompressorWinFormsApp -{ - using HuffmanCompressorLib; - - public partial class MainForm : Form - { - public MainForm() - { - InitializeComponent(); - } - - private void btnCompressFile_Click(object sender, EventArgs e) - { - openFileDialog.Filter = "All files (*.*)|*.*"; - var openFileDialogResult = openFileDialog.ShowDialog(this); - if (openFileDialogResult != DialogResult.OK) - { - return; - } - - var compressor = new HuffmanCompressor(); - toolStripStatusLabel.Text = "Compressing..."; - compressor.Compress(openFileDialog.FileName, openFileDialog.FileName + ".huf"); - toolStripStatusLabel.Text = "Ready"; - } - - private void btnInflateFile_Click(object sender, EventArgs e) - { - openFileDialog.Filter = "Compressed files (*.huf)|*.huf"; - openFileDialog.AddExtension = true; - var openFileDialogResult = openFileDialog.ShowDialog(this); - if (openFileDialogResult != DialogResult.OK) - { - return; - } - - var compressor = new HuffmanCompressor(); - toolStripStatusLabel.Text = "Inflating..."; - compressor.Inflate(openFileDialog.FileName, openFileDialog.FileName + ".inflated"); - toolStripStatusLabel.Text = "Ready"; - } - } -} \ No newline at end of file