Skip to content

Commit

Permalink
rle compressor unit tests and refactoring
Browse files Browse the repository at this point in the history
code of rle compressor was simplified and refactored using C# specific features, unit tests were added for various edge cases
  • Loading branch information
VasilijP committed Oct 18, 2024
1 parent 2309a06 commit e9082df
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 59 deletions.
193 changes: 193 additions & 0 deletions tgalib-core.Tests/RleCompressorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using NUnit.Framework;
using System.IO;

namespace tgalib_core.Tests
{
[TestFixture]
public class RleCompressorTests
{
[Test]
public void TestEmptyInput()
{
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

compressor.ForceWrite(); // Should handle empty input gracefully

Assert.That(0, Is.EqualTo(ms.Length), "Output stream should be empty for empty input.");
}

[Test]
public void TestSinglePixel()
{
byte[] pixel = [0xAA];
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

compressor.Write(pixel);
compressor.ForceWrite();

byte[] expectedOutput1 = [0x00, 0xAA]; // Raw packet with one pixel
byte[] expectedOutput2 = [ 128, 0xAA]; // Run packet with one pixel
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.AnyOf(expectedOutput1, expectedOutput2));
}

[Test]
public void TestRunLengthPacket()
{
byte[] pixel = [0xAA];
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

// Write the same pixel 5 times
for (int i = 0; i < 5; i++) { compressor.Write(pixel); }
compressor.ForceWrite();

// Rep starts at 0 and increments 4 times (total repetitions: 4)
// Header: 128 | Rep (Rep = 4)
byte[] expectedOutput = [128 | 4, 0xAA]; // Run-length packet
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.EqualTo(expectedOutput));
}

[Test]
public void TestRunLengthPacket_MaxRun()
{
byte[] pixel = [0xAA];
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

// Maximum run-length
for (int i = 0; i < 128; i++) { compressor.Write(pixel); }
compressor.ForceWrite();

// Rep increments from 0 to 127 (total repetitions: 127) -> Header: 128 | 127 = 255
byte[] expectedOutput = [255, 0xAA]; // Run-length packet
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.EqualTo(expectedOutput));
}

[Test]
public void TestRunLengthPacket_ExceedMaxRun()
{
byte[] pixel = [0xAA];
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

// Exceed maximum run-length
for (int i = 0; i < 130; i++) { compressor.Write(pixel); }
compressor.ForceWrite();

// First packet: Rep = 127 (header: 255), pixel: 0xAA (128 pixels)
// Second packet: Rep = 1 (header: 130), pixel: 0xAA (2 pixels) -> 130 pixels in total
byte[] expectedOutput = [255, 0xAA, 128 | 1, 0xAA];
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.EqualTo(expectedOutput));
}

[Test]
public void TestRawPacket()
{
byte[] pixel1 = [0xAA];
byte[] pixel2 = [0xBB];
byte[] pixel3 = [0xCC];
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

compressor.Write(pixel1);
compressor.Write(pixel2);
compressor.Write(pixel3);
compressor.ForceWrite();

// Raw packet with 3 pixels (header: 2)
byte[] expectedOutput = [0x02, 0xAA, 0xBB, 0xCC];
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.EqualTo(expectedOutput));
}

[Test]
public void TestMixedPackets()
{
byte[] pixelA = [0xAA];
byte[] pixelB = [0xBB];
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

// Raw pixels
compressor.Write(pixelA);
compressor.Write(pixelB);
compressor.Write(pixelA);

// Run-length pixels
for (int i = 0; i < 5; i++) { compressor.Write(pixelB); }
compressor.ForceWrite();

// Raw packet: header 2, data: 0xAA, 0xBB, 0xAA
// Run-length packet: header 132 (128 | 4), data: 0xBB
byte[] expectedOutput = [0x02, 0xAA, 0xBB, 0xAA, 132, 0xBB];
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.EqualTo(expectedOutput));
}

[Test]
public void TestMaxRawPacket()
{
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

// Raw packet with maximum length (128 pixels)
for (int i = 0; i < 128; i++) { compressor.Write([(byte)i]); }
compressor.ForceWrite();

byte[] expectedOutput = new byte[1 + 128];
expectedOutput[0] = 127; // Header: 128 - 1
for (int i = 0; i < 128; i++) { expectedOutput[1 + i] = (byte)i; }
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.EqualTo(expectedOutput));
}

[Test]
public void TestRawPacket_ExceedMaxLength()
{
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

// Write 130 different pixels
for (int i = 0; i < 130; i++) { compressor.Write([(byte)i]); }
compressor.ForceWrite();

// First packet: header 127, data: bytes 0-127
// Second packet: header 1, data: bytes 128-129
byte[] expectedOutput = new byte[1 + 128 + 1 + 2];
expectedOutput[0] = 127;
for (int i = 0; i < 128; i++) { expectedOutput[1 + i] = (byte)i; }
expectedOutput[129] = 1; // Header for second packet
expectedOutput[130] = 128;
expectedOutput[131] = 129;
byte[] actualOutput = ms.ToArray();

Assert.That(actualOutput, Is.EqualTo(expectedOutput));
}

[Test]
public void TestInputFillsBuffer()
{
const int totalPixels = RleCompressor.Buflen + 1000; // Exceeds buffer length
using MemoryStream ms = new();
RleCompressor compressor = new(ms);

for (int i = 0; i < totalPixels; i++) { compressor.Write([(byte)(i % 256)]); }
// compressor.ForceWrite(); <- This is needed just at the end (when processing an image), the buffer is automatically flushed when it's full during writing.

Assert.That(ms.Length > 0, "Output stream should contain data.");
}
}
}
2 changes: 1 addition & 1 deletion tgalib-core.Tests/TgaImageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void TestGetBitmap(string filename, string expected, bool useAlphaForcefu
using FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
using BinaryReader r = new BinaryReader(fs);

/* TODO:
/* TODO: implement tests actually comparing reference image for each test case.
var expectedImage = new BitmapImage(new Uri(expected, UriKind.Relative));
var tga = new TgaImage(r, useAlphaForcefully);
var actualImage = tga.GetBitmap();
Expand Down
76 changes: 25 additions & 51 deletions tgalib-core/RleCompressor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,43 @@
//auxiliary class for RLE compression
public class RleCompressor(Stream stream)
{
private const int buflen = 65536;
private const int bufcritical = 65532;
private RlePixel[] buf = new RlePixel[buflen];
public const int Buflen = 8*1024;
private readonly RlePixel[] buf = new RlePixel[Buflen];
private int bufindex = 0;//index of first free place

private bool match(RlePixel arg1, RlePixel arg2)
{
if (arg1.c.Length != arg2.c.Length) return(false);
for (int i = 0; i < arg1.c.Length; i++) if (arg1.c[i] != arg2.c[i]) return(false);
return(true);
}

public void write(byte arg) { write([arg]); }
public void Write(byte arg) { Write([arg]); }

//add arg to FIFO
public void write(byte[] arg)
{
RlePixel argpix = new RlePixel(arg);
if ((bufindex > 0)&&match(buf[bufindex-1],argpix)){
if (buf[bufindex-1].rep < 128){
buf[bufindex-1].rep++;
return;
}
}
buf[bufindex]=argpix;
bufindex++;
if (bufindex > bufcritical) forceWrite();
public void Write(byte[] arg)
{
if (bufindex > 0 && buf[bufindex-1].Rep < 127 && buf[bufindex-1].C.SequenceEqual(arg)) { buf[bufindex-1].Rep++; return; } // increment repetition counter
buf[bufindex++] = new RlePixel(arg);
if (bufindex == Buflen) { ForceWrite(); }
}

private static readonly byte[] Header = new byte[1];

//write one copy or run packet
private void writePacket(int from, int to)
private void WritePacket(int from, int to)
{
byte[] header = new byte[1];
if (to - from > 0) //create copy packet
{
header[0] = (byte)(to - from);//number of copied pixels - 1
} else { //write run packet
header[0] = (byte)(128 | (buf[from].rep-1));//number of repetition -1 and highest bit is 1
}
try
{
stream.Write(header);
for (int rpc = from; rpc <= to; rpc++) // this should run only once for run packet
{
stream.Write(buf[rpc].c);
}
} catch (IOException e) { Console.WriteLine(e); }
if (to - from > 0) { Header[0] = (byte)(to - from); } // create copy packet, number of copied pixels - 1
else { Header[0] = (byte)(128 | buf[from].Rep); } // write run packet, number of repetition -1 and highest bit is 1
stream.Write(Header);
for (int rpc = from; rpc <= to; rpc++) { stream.Write(buf[rpc].C); } // this should run only once for run packet
}

//forced write of entire buffer to output (end of image or full buffer)
public void forceWrite()
{
public void ForceWrite()
{
int from = 0;
while (from < bufindex)
{
int to = from;
while ((to < bufindex-1)&&(buf[to].rep == 1)&&(to-from < 127)) to++;
if ((to - from +1 > 1)&&(buf[to].rep > 1)) to--;
writePacket(from, to);
from = to+1;
//writePacket(from, from);
//from++;
{
int to = from;
while (buf[to].Rep == 0 && to+1 < bufindex && to-from < 127) { to++; }
if (to - from > 0 && buf[to].Rep > 0) { to--; }
WritePacket(from, to);
from = to+1;
}
bufindex = 0;//empty buffer
bufindex = 0; //empty buffer
}
}
}
7 changes: 5 additions & 2 deletions tgalib-core/RlePixel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
// Pixel value with repetition count.
public class RlePixel(byte[] arg)
{
public byte[] c = arg;
public int rep = 1;
// data for a single pixel
public readonly byte[] C = arg;

// repetition count (on top of the single pixel)
public int Rep = 0;
}
10 changes: 5 additions & 5 deletions tgalib-core/TgaFileFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ private static void TgaRgb24RleSave(Stream tgaFile, TgaImage image)
tgaFile.Write(header);

RleCompressor rlec = new(tgaFile);
for (int y = 0; y < height; ++y)
for (int y = 0; y < height; ++y) // TODO: according to v2 spec, there should not be a run overlapping 2 scanlines
for (int x = 0; x < width; ++x)
{
image.GetPixelRgba(x, y, out int r, out int g, out int b, out int a);
rlec.write([(byte)(b & 255), (byte)(g & 255), (byte)(r & 255)]);
rlec.Write([(byte)(b & 255), (byte)(g & 255), (byte)(r & 255)]);
}

rlec.forceWrite();
rlec.ForceWrite();
tgaFile.Close();
}

Expand Down Expand Up @@ -119,10 +119,10 @@ private static void TgaPal8RleSave(Stream tgaFile, TgaImage image)
{
image.GetPixelRgba(x, y, out int r, out int g, out int b, out int a);
string key = $"{r & 255}|{g & 255}|{b & 255}";
rlec.write(hm.TryGetValue(key, out int value)?(byte)value:(byte)0); // write index of a color in palette
rlec.Write(hm.TryGetValue(key, out int value)?(byte)value:(byte)0); // write index of a color in palette
}

rlec.forceWrite();//write last pixels
rlec.ForceWrite();//write last pixels
tgaFile.Close();
}

Expand Down

0 comments on commit e9082df

Please sign in to comment.