Skip to content

Commit

Permalink
Implement PCX recompression and (Windows-only) file previews…
Browse files Browse the repository at this point in the history
GTKSharp (via Eto.Forms) and Magick.NET clash due to an issue with
glib throwing an uncaught error (dlemstra/Magick.NET#1541). Since
C# doesn't support PCX and I haven't found a decent conversion
library to bypass this, we're stuck with the situation until I can
find a workaround. Unclear how this affects MacOS as I don't have a
test environment there.
  • Loading branch information
Shiryou committed Oct 13, 2024
1 parent 4603335 commit 6d4c477
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 7 deletions.
10 changes: 7 additions & 3 deletions brut-cli/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,19 @@ static void Main(string[] args)
Console.WriteLine(" + add file");
Console.WriteLine(" - remove file");
Console.WriteLine(" c compress resources (default)");
Console.WriteLine(" e extract file (does not remove it)");
Console.WriteLine(" x extract all files (does not remove them)");
Console.WriteLine(" e extract file");
Console.WriteLine(" x extract all files");
Console.WriteLine(" hc use CRC hash (default)");
Console.WriteLine(" hi use ID hash");
Console.WriteLine(" l list contents of resource file");
Console.WriteLine(" n do not rotate PCX resources (default)");
Console.WriteLine(" r rotate PCX resources");
Console.WriteLine(" s nnnnn max size of resource permitted");
Console.WriteLine(" t attempt to restore a PCX file");
Console.WriteLine(" u do not compress resources");
Console.WriteLine(" v verify resource file");
Console.WriteLine(" @ respfile run commands in respfile");
Console.WriteLine("Note: Compression is not yet supported when adding a file.");
Console.WriteLine("Note: Compression and respfiles are not yet supported.");
return;
}

Expand Down Expand Up @@ -115,6 +116,9 @@ static void Main(string[] args)
case 'N':
ru.DisablePCXRotation();
break;
case 'T':
ru.RestorePCX();
break;
}

if (file_operation)
Expand Down
1 change: 1 addition & 0 deletions brut-gui/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public void OpenFileCommand_Executed(object? sender, EventArgs e)
Globals.resource.Dispose();
}
Globals.resource = new ResourceUtility(openDialog.FileName);
Globals.resource.RestorePCX();
Globals.resourceName = Path.GetFileName(openDialog.FileName).ToUpper();
form.ManageFileDependentFields(true);
}
Expand Down
20 changes: 20 additions & 0 deletions brut-gui/MainForm.eto.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Drawing;
using System.IO;
using System.Collections.Generic;
using System.Runtime.InteropServices;

using Eto.Forms;
using ImageMagick;

using ResourceUtilityLib;

Expand All @@ -19,6 +22,7 @@ partial class MainForm : Eto.Forms.Form
public List<object> selected_dependent = new();
public ListBox listBox = new();
public Label fileInfo = new();
public ImageView preview = new();
public ResourceHeader selected;
public Commands commands;
public MenuBar menuBar;
Expand Down Expand Up @@ -88,6 +92,11 @@ public TableLayout InitializePanels()
ScaleHeight = true
});

fileManager.Rows.Add(new TableRow(new TableCell(preview, true))
{
ScaleHeight = true
});

fileManager.Rows.Add(new TableRow(new TableCell(BuildFileControlButtons(), true)));
layout.Rows.Add(new TableRow(new TableCell(listBox, true), new TableCell(fileManager, true)));

Expand Down Expand Up @@ -144,6 +153,17 @@ public void ShowFileInfo(object? sender, EventArgs e)
metadata += String.Format("File size: {0}", selected.cbUncompressedData);
}

if (ResourceUtility.GetSupportedExtensions()[selected.extension] == "PCX" && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Convert from internal bitmap back to PCX and then to standard Bitmap
byte[] data = Globals.resource.GetResourceData(selected);
MagickImage image = new(data, MagickFormat.Pcx);
image.Format = MagickFormat.Bmp;
preview.Image = new Eto.Drawing.Bitmap(image.ToByteArray());
} else {
metadata += String.Format("\n\nPCX previews are currently unavailable on Linux builds due to technical issues.\nPlease extract the file(s) and view them with an image viewer with PCX support.");
}

fileInfo.Text = metadata;
}

Expand Down
1 change: 1 addition & 0 deletions brut-gui/brut-gui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.0.0" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.143">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
179 changes: 178 additions & 1 deletion brut-lib/ImageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public struct PCXHeader

/// <summary>
/// Describes the header of a bitmap file.
///
/// Note: "Bitmap" in this context refers to a non-standard format used as an intermediary.
/// </summary>
public struct BitmapHeader
{
Expand Down Expand Up @@ -52,11 +54,23 @@ public ImageHandler(byte[] input)
/// </summary>
/// <param name="data">PCX data.</param>
/// <param name="rotate">Whether to rotate the image.</param>
/// <returns></returns>
/// <returns>The bitmap file data as a byte array.</returns>
public static byte[] ConvertPCXToBitmap(byte[] data, bool rotate = false)
{
return new PCX(data).ConvertToBitmap(rotate);
}

/// <summary>
/// Converts bitmap data to a PCX
/// </summary>
/// <param name="data">bitmap data.</param>
/// <param name="rotate">Whether to unrotate the image.</param>
/// <returns>The PCX file data as a byte array.</returns>

public static byte[] ConvertBitmapToPCX(byte[] data, bool rotate = false)
{
return new Bitmap(data).ConvertToPCX(rotate);
}
}

/// <summary>
Expand Down Expand Up @@ -248,4 +262,167 @@ private void Decompress()
};
}
}

/// <summary>
/// Provides functions for bitmap conversion to PCX.
/// </summary>
public class Bitmap : ImageHandler
{
private readonly BitmapHeader header;
private PCXHeader pcx_header;
private readonly uint decompressed_length; // expected length of bitmap data
private readonly uint min_run = 2;
private readonly uint max_run = 63;
private bool short_width = true;

/// <summary>
/// Inititalizes the image and buffer dimensions.
/// </summary>
/// <param name="input">PCX data.</param>
public Bitmap(byte[] input) : base(input)
{
header = ReadBitmapHeader();
decompressed_length = (uint)input.Length - bitmap_header_length;
}

/// <summary>
/// Perform the converstion from PCX to bitmap.
/// </summary>
/// <param name="rotate">Whether to rotate the image.</param>
/// <returns>The PCX image data as a byte array.</returns>
public byte[] ConvertToPCX(bool derotate)
{
if (derotate)
{
Derotate();
}
else
{
Recompress();
}
output.Write((byte)0x0C); // palette separator
output.Write(Convert.FromBase64String("AAD/CwsLExMTGxsbIyMjKysrNzc3Pz8/R0dHT09PV1dXY2Nja2trc3Nze3t7g4ODj4+Pk5OTm5ubo6Ojq6urs7Ozu7u7w8PDy8vLz8/P19fX39/f5+fn7+/v9/f3////IxsvJx83Lyc/NytHPzNTRzdbTz9jV0drX0t3Z1N/b1uHd2OTf2ubh3Ojk3urm4O3o4u/r5PHt5vPw6Pbz6vj17Pr47vz78f/Fxc7GxtHHx9XIyNnKyd3LyuDMy+TNzOjOzezPzu3R0O7T0u/V1PHX1vLZ2PPb2vTd3fbf3/fi4vjk5Prn5/vp6fzs7P3v7//Lws7NwtHQw9TSxNfVxNvYxd7axeHdxuTgx+jix+vlyO7nyPLqyfXtyvjvyvvyy//yzf/z0P/00v/11f/21//32v/33P/43//OwsLRwsLUwsLXw8Paw8Pdw8Pgw8Pjw8Plw8PoxMTrxcXtxsbwyMjyycn1y8v2zMz3zc34zs750ND60dH809P91NT+1dX/19fLxcAOxsARyMAUysAYy8AbzcAezsAh0MAl0sAo1MAr1cAu18Hy2cH12sH43MH73sH/4ML/48L/5sP/6cT/7MT/78X/8sb/9cfLyMLNysLQzMPTz8TW0sXZ1MXc18bf2sfi3Mjl38jo4snr5Mru58vx6sz07c338M748s/69NH899P8+Nj9+d79+uT+++v//fHACMLACsLADMPADsPAEMTAEsTAFMXAFsXAGMbAGsbAHMfAHsfAH8fAIcfAI8jAJcjB58nC6svE7c7G8NDJ8tLM9dXP+NjS+9vLyMXNycbPy8fRzcjUz8nW0crY08va1c3d187f2M/g2tDi3NHk3tPm39To4dXq49bs5dju59nw6dvy7N317t/38eH58+P89uXLx8bNyMfQysjSy8nVzcrXz8va0Mzd0s3f087h1M/j1tDm19Ho2NLq2tPs29Tu3NXx3tfz4dn149v45t366N/86+H/7uT/8OX64cA76MA98MA/+MA//8A+/9////T////"));
SavePCXHeader();
return ((MemoryStream)output.BaseStream).ToArray();
}

/// <summary>
/// Write the header of a PCX file.
/// </summary>
private void SavePCXHeader()
{
output.BaseStream.Position = 0;
output.Write(0x0801050a);
output.Write(pcx_header.XOrigin);
output.Write(pcx_header.YOrigin);
output.Write((short)(pcx_header.Width-1));
output.Write((short)(pcx_header.Height-1));
output.Write(0x01e00280);
output.Write(new byte[2]);
// write a common palette
output.Write(Convert.FromBase64String("/wsLCxMTExsbGyMjIysrKzc3Nz8/P0dHR09PT1dXV2NjY2tra3Nzc3t7e4ODgw=="));
output.Write((byte)0);
output.Write((byte)1);
output.Write(pcx_header.LineLength);
}

/// <summary>
/// Read the header of a bitmap file.
/// </summary>
/// <returns>The bitmap header</returns>
private BitmapHeader ReadBitmapHeader()
{
data.BaseStream.Position = 0;
BitmapHeader header = new()
{
Width = data.ReadInt16(),
Height = data.ReadUInt16(),
Scale = data.ReadInt16(),
CenterPoint = data.ReadInt16(),
Type = data.ReadInt16()
};

return header;
}

/// <summary>
/// Derotate the PCX data into a bitmap.
/// </summary>
private void Derotate()
{
}

/// <summary>
/// Convert the bitmap data into a PCX without rotating.
/// </summary>
private void Recompress()
{
pcx_header = new()
{
Code = 10,
XOrigin = 0,
YOrigin = 0,
Width = header.Width,
Height = (short)header.Height,
LineLength = (ushort)header.Width
};

// Check if the last byte of each line is 0 to adjust the width.
for (int i = 0; i < pcx_header.Height; i++)
{
data.BaseStream.Position = (i * pcx_header.Width) + (pcx_header.Width - 1);
if (data.ReadByte() != 0)
{
short_width = false;
break;
}
}
if (short_width)
{
pcx_header.Width = (short)(pcx_header.Width - 1);
}

// decompress
data.BaseStream.Position = bitmap_header_length;
output.BaseStream.Position = pcx_reserved;
while (data.BaseStream.Position < data.BaseStream.Length)
{
//if (data.BaseStream.Position % pcx_header.LineLength == 0)
//{
// data.ReadByte();
//}
output.Write(EvaluateRun());
}
}

/// <summary>
/// Check whether a single byte or a run length/byte pair should be written
/// </summary>
private byte[] EvaluateRun()
{
long start_pos = data.BaseStream.Position;
byte start = data.ReadByte();
int count = 1;
while (
data.BaseStream.Position < data.BaseStream.Length &&
data.ReadByte() == start &&
count < max_run)
{
count++;
}

if (count < min_run)
{
count = 1;
}
data.BaseStream.Position = start_pos + count;
// 1 in two most significant bits indicates a run, so any single value greater
// than 191 must be stored in a byte pair
if (count == 1 && start < 192)
{
return [start];
}
return [(byte)(192+count), start];
}
}
}
32 changes: 29 additions & 3 deletions brut-lib/Library.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public class ResourceUtility : IDisposable

private bool compress = true;
private bool rotate = false;
private bool restore = false;

private uint file_version;
private uint directory;
Expand Down Expand Up @@ -221,6 +222,16 @@ public void DisablePCXRotation()
rotate = false;
}

public void RestorePCX()
{
restore = true;
}

public void RetainBitmap()
{
restore = false;
}

/// <summary>
/// Loads the header for a resource contained in a resource file and performs some sanity checks.
/// </summary>
Expand Down Expand Up @@ -752,15 +763,30 @@ public byte[] GetResourceData(ResourceHeader header)
{
resource_file.BaseStream.Position = GetFileOffset(header.filename) + size_of_rheader;
byte[] compressed_data = resource_file.ReadBytes((int)header.cbCompressedData);
byte[] uncompressed_data;
if ((CompressionTypes)header.compressionCode == CompressionTypes.NoCompression)
{
return compressed_data;
uncompressed_data = compressed_data;
}
else if ((CompressionTypes)header.compressionCode == CompressionTypes.LZSSCompression)
{
return LZSS.Decode(compressed_data, header.cbUncompressedData);
uncompressed_data = LZSS.Decode(compressed_data, header.cbUncompressedData);
}
else
{
return new byte[0];
}
return new byte[0];
if (restore)
{
bool uncompressed = ((header.flags & (byte)1) == (byte)1);
bool rotated = ((header.flags & (byte)2) == (byte)1);
if (uncompressed)
{
return ImageHandler.ConvertBitmapToPCX(uncompressed_data);
}
}
return uncompressed_data;

}

/// <summary>
Expand Down

0 comments on commit 6d4c477

Please sign in to comment.