Skip to content

Instantly share code, notes, and snippets.

@trigger-segfault
Created September 8, 2020 07:49
Show Gist options
  • Select an option

  • Save trigger-segfault/6872978a837aa64994b266b0eafdfdca to your computer and use it in GitHub Desktop.

Select an option

Save trigger-segfault/6872978a837aa64994b266b0eafdfdca to your computer and use it in GitHub Desktop.
Two-way implementation for processing CatSystem HG-2/3 image encodings
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
// THIS IS A WORK IN PROGRESS
// Class for creating and extracting HG-2/3 specific image encodings
// This does not handle reading the container file format, nor Zlib compression.
namespace TriggersTools.CatSystem2 {
[Flags]
public enum HgxOptions {
None = 0,
Flip = (1 << 0), // Flip vertically (applied after crop when encoding)
Crop = (1 << 2), // Expand or Shrink
}
// A decompressed HG-2/HG-3 image slice
public struct HgxSlice {
#region Fields
public int Index { get; set; }
public int Length { get; set; }
// public int SliceIndex { get; set; }
// public int SliceLength { get; set; }
// public int OrigDataLength { get; set; }
// public int OrigCmdLength { get; set; }
public byte[] Data { get; set; }
public byte[] Cmd { get; set; }
#endregion
}
public class HgxImage {
#region Static Fields
private static readonly uint[,] _weightTables; // uint[4, 256]
private static readonly byte[,] _absTables; // byte[2, 256] - [0] Forward, [1] Inverse
#endregion
#region Fields
public Size Size { get; set; }
public int BitDepth { get; set; }
public Size FullSize { get; set; }
public Point Offset { get; set; }
public bool HasTransparency { get; set; }
public Point Base { get; set; } // image origin/base (applied based on full image size?)
public HgxSlice[] Slices { get; set; }
#endregion
#region Properties
public int Width => this.Size.Width;
public int Height => this.Size.Height;
public int FullWidth => this.FullSize.Width;
public int FullHeight => this.FullSize.Height;
public int OffsetX => this.Offset.X;
public int OffsetY => this.Offset.Y;
public int BaseX => this.Base.X;
public int BaseY => this.Base.Y;
public int ByteDepth => (this.BitDepth + 7) / 8;
// In reality, CatSystem does not handle bitdepths that don't align to the byte
// It calculates stride like so:
public int Stride => (this.Size.Width * this.ByteDepth + 3) & ~0x3;
//public int Stride => ((this.Size.Width * this.BitDepth + 7) / 8 + 3) & ~0x3;
#endregion
#region Constructors
static HgxImage() {
HgxImage._weightTables = HgxImage.InitWeightTables();
HgxImage._absTables = HgxImage.InitAbsoluteTables();
}
public HgxImage(Size size, int bitDepth)
: this(size, bitDepth, Point.Empty, size)
{
}
public HgxImage(Size size, int bitDepth)
: this(size, bitDepth, Point.Empty, size)
{
}
public HgxImage(Size size, int bitDepth, Point offset, Size fullSize) {
if (size.Width <= 0 || size.Height <= 0)
throw new ArgumentException("Size width or height is less than or equal to zero", nameof(size));
if (bitDepth <= 0)
throw new ArgumentException("BitDepth is less than or equal to zero", nameof(bitDepth));
if (fullSize.Width <= 0 || fullSize.Height <= 0)
throw new ArgumentException("FullSize width or height is less than or equal to zero", nameof(fullSize));
this.Size = size;
this.BitDepth = bitDepth;
this.Offset = offset;
this.FullSize = fullSize;
this.HasTransparency = false;
this.Base = Point.Empty;
this.Slices = new HgxSlice[1]{ new HgxSlice() };
}
#endregion
#region Public Methods
// operates on/modifies pixels buffer
public void EncodePixels(byte[] pixels) {
for (int i = 0; i < this.Slices.Length; i++) {
this.Slices[i] = this.EncodeSlice(this.Slices[i], pixels);
}
}
public void DecodePixels(byte[] pixels) {
for (int i = 0; i < this.Slices.Length; i++) {
this.DecodeSlice(this.Slices[i], pixels);
}
}
public HgxSlice EncodeSlice(HgxSlice slice, byte[] pixels) {
this.EncodeDelta(slice, pixels);
this.EncodeBlocks(slice, pixels, out byte[] blocks);
return this.EncodeZeroRunLength(slice, blocks);
}
public void DecodeSlice(HgxSlice slice, byte[] pixels) {
this.DecodeZeroRunLength(slice, out byte[] blocks);
this.DecodeBlocks(slice, pixels, blocks);
this.DecodeDelta(slice, pixels);
}
public static HgxImage FromBitmap(Bitmap source, HgxOptions options, Point origin = default(Point)) {
// SETUP
int bitDepth;
bool hasTransparency;
PixelFormat format;
Rectangle bounds = new Rectangle(0, 0, source.Width, source.Height);
switch (source.PixelFormat) {
case PixelFormat.Format24bppRgb:
case PixelFormat.Format32bppRgb:
format = PixelFormat.Format24bppRgb;
hasTransparency = false;
bitDepth = 24;
break;
case PixelFormat.Format32bppArgb:
case PixelFormat.Format32bppPArgb:
format = PixelFormat.Format32bppArgb;
bitDepth = 32;
if (options.HasFlag(HgxOptions.Crop)) {
bounds = HgxImage.GetBoundingBox(source, out hasTransparency);
}
else {
hasTransparency = HgxImage.HasTransparency(source);
}
break;
default:
throw new Exception("Unsupported pixel format");
}
if (!options.HasFlag(HgxOptions.Flip)) {
// This image type is stored flipped, so reverse the option
target.RotateFlip(RotateFlipType.RotateNoneFlipY);
if (options.HasFlag(HgxOptions.Crop)) {
// Flip is applied after cropping, so update bounds
int newY = source.Height - bounds.Bottom;
bounds = new Rectangle(bounds.X, newY, bounds.Width, bounds.Height);
}
}
byte[] pixels;
BitmapData data = source.LockBits(bounds, ImageLockMode.ReadOnly, format);
try {
pixels = new byte[data.Stride * data.Height];
Marshal.Copy(data.Scan0, pixels, 0, pixels.Length);
} finally {
source.UnlockBits(data);
}
HgxImage encoder = new HgxImage(bounds.Size, bitDepth, source.Size, bounds.Location) {
HasTransparency = hasTransparency,
Base = origin,
Slices = new HgxSlice[1]{ new HgxSlice{
Index = 0,
Length = bounds.Height,
}},
};
encoder.EncodePixels(pixels);
return encoder;
}
public Bitmap ToBitmap(HgxOptions options) {
// SETUP
PixelFormat format;
switch (this.BitDepth) {
case 24: format = PixelFormat.Format24bppRgb; break;
case 32: format = PixelFormat.Format32bppArgb; break;
default: throw new Exception("Unsupported bit depth");
}
if (this.Size == this.FullSize && this.Offset.IsEmpty)
options &= ~HgxOptions.Crop; // Remove unnecessary crop option
bool crop = options.HasFlag(HgxOptions.Crop);
Size targetSize = (crop ? this.FullSize : this.Size);
Point targetOffset = (crop ? this.Offset : Point.Empty);
PixelFormat targetFormat = (crop ? PixelFormat.Format32bppArgb : format);
// DECODING PROCESS
byte[] pixels = new byte[this.Stride * this.Height];
this.DecodePixels(pixels);
// HGX OPTIONS
GCHandle handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
try {
IntPtr scan0 = handle.AddrOfPinnedObject();
using (var intermediate = new Bitmap(this.Width, this.Height, this.Stride, format, scan0)) {
Bitmap target = new Bitmap(targetSize.Width, targetSize.Height, targetFormat);
try {
using (Graphics g = Graphics.FromImage(targetUncrop)) {
if (!options.HasFlag(HgxOptions.Flip))
// This image type is stored flipped, so reverse the option
intermediate.RotateFlip(RotateFlipType.RotateNoneFlipY);
g.DrawImageUnscaled(intermediate, targetOffset.X, targetOffset.Y);
return target;
}
}
catch {
target.Dispose();
throw;
}
}
} finally {
// Thing to note that gave me headaches earlier:
// Once this handle is freed, the bitmap loaded from
// scan0 will be invalidated after garbage collection.
handle.Free();
}
}
#endregion
#region Private Decoding Methods
private HgxSlice EncodeZeroRunLength(HgxSlice slice, byte[] buffer) {
int length = buffer.Length;
// first measure the size of the data buffer and cmd bits, and record the runs
int dataLength = 0;
long cmdBitLength = 1L; // 1 bit consumed for copyFlag
cmdBitLength += BitBuffer.MeasureEliasGamma(length);
List<int> runs = new List<int>(); // includes zero and non-zero runs
bool copyFlag = (buffer[0] != 0); // is first run non-zero data?
int offset = 0;
while (offset < length) {
int runLength = 1;
if (copyFlag) {
while (offset + runLength < length && buffer[offset + runLength] != 0)
runLength++;
dataLength += runLength;
}
else {
while (offset + runLength < length && buffer[offset + runLength] == 0)
runLength++;
}
runs.Add(runLength);
cmdBitLength += BitBuffer.MeasureEliasGamma(runLength);
offset += runLength;
copyFlag = !copyFlag;
}
// now create the buffers and write the data
data = new byte[dataLength];
cmd = new byte[checked((int) ((cmdBitLength + 7) / 8))];
copyFlag = (buffer[0] != 0);
BitBuffer bitbuf = new BitBuffer(cmd, cmd.Length);
bitbuf.WriteFlag(copyFlag);
bitbuf.WriteEliasGamma(length);
offset = 0;
int dataOffset = 0;
for (int j = 0; j < runs.Length; j++) {
int runLength = runs[j];
if (copyFlag) {
Array.Copy(buffer, offset, data, dataOffset, runLength);
dataOffset += runLength;
}
bitbuf.WriteEliasGamma(runLength);
offset += runLength;
copyFlag = !copyFlag;
}
slice.Data = data;
slice.Cmd = cmd;
return slice;
}
private void DecodeZeroRunLength(HgxSlice slice, out byte[] buffer) {
BitBuffer bitbuf = new BitBuffer(cmd, cmd.Length);
int offset = 0;
int dataOffset = 0;
bool copyFlag = bitbuf.ReadFlag();
int length = bitbuf.ReadEliasGamma();
while (offset < length) {
int runLength = bitbuf.ReadEliasGamma();
if (copyFlag) {
Array.Copy(data, dataOffset, buffer, offset, runLength);
dataOffset += runLength;
}
offset += runLength;
copyFlag = !copyFlag;
}
}
private void EncodeBlocks(HgxSlice slice, byte[] pixels, out byte[] blocks) {
byte[,] absTables = HgxImage._absTables; // [0] for normalization
int stride = this.Stride;
int bufferStart = slice.Index * stride;
int bufferEnd = (slice.Index + slice.Length) * stride;
int sectLength = slice.Length * stride / 4;
// First loop through the entire pixels slice and perform absolute transform
for (int i = bufferStart; i < bufferEnd; i++) {
//byte a = pixels[i];
//pixels[i] = unchecked((byte) ((a << 1) & 0xfe) ^ ((a & 0x80) != 0 ? (byte) 0xff : (byte) 0x00));
pixels[i] = absTables[0, pixels[i]];
}
// Iterate through each section one at a time, each pass
// through pixels encodes a different mask (section/block) of bytes
int dst = 0;
for (int k = 6, dst = 0; k >= 0; k-=2) {
for (int i = 0, src = 0; i < sectLength; i++) {
byte val = 0;
// Take next 4 source bytes and convert to weighted section byte
for (int j = 0; j < 8; j+=2) {
byte b = pixels[src++];
val |= unchecked((byte) (((b >> k) & 0x03) << j));
}
blocks[dst++] = val;
}
}
}
private void DecodeBlocks(HgxSlice slice, byte[] pixels, byte[] blocks) {
uint[,] tables = HgxImage._weightTables;
byte[,] absTables = HgxImage._absTables; // [1] for inverse normalization
int stride = this.Stride;
int bufferStart = slice.Index * stride;
int sectLength = slice.Length * stride / 4;
int sect0 = 0;
int sect1 = sect0 + sectLength;
int sect2 = sect0 + sectLength * 2;
int sect3 = sect0 + sectLength * 3;
for (int i = 0, j = bufferStart; i < sectLength; i++, j+=4) {
uint val = tables[0, blocks[sect0 + i]] | tables[1, blocks[sect1 + i]] |
tables[2, blocks[sect2 + i]] | tables[3, blocks[sect3 + i]];
pixels[j] = absTables[1, unchecked((byte) val)];
pixels[j + 1] = absTables[1, unchecked((byte) (val >> 8))];
pixels[j + 2] = absTables[1, unchecked((byte) (val >> 16))];
pixels[j + 3] = absTables[1, unchecked((byte) (val >> 24))];
}
}
private void EncodeDelta(HgxSlice slice, byte[] pixels) {
int stride = this.Stride;
int byteDepth = this.ByteDepth;
int bufferStart = slice.Index * stride;
int bufferEnd = (slice.Index + slice.Length) * stride;
Array.Copy(pixels, bufferStart, pixels, 0, bufferStart - bufferEnd);
pixels = (byte[]) pixels.Clone();
// delta RGBA channels of each previous stride in all but first row
for (int xy = bufferEnd - 1; xy >= bufferStart + stride; xy--) {
pixels[xy] = unchecked((byte) (pixels[xy] - pixels[xy - stride]));
}
// delta RGBA channels of each previous pixel in first row
for (int x0 = bufferStart + stride - 1; x0 >= bufferStart + byteDepth; x0--) {
pixels[x0] = unchecked((byte) (pixels[x0] - pixels[x0 - byteDepth]));
}
}
private void DecodeDelta(HgxSlice slice, byte[] pixels) {
int stride = this.Stride;
int byteDepth = this.ByteDepth;
int bufferStart = slice.Index * stride;
int bufferEnd = (slice.Index + slice.Length) * stride;
Array.Copy(buffer, 0, pixels, bufferStart, bufferStart - bufferEnd);
// inverse delta RGBA channels of each previous pixel in first row
for (int x0 = bufferStart + byteDepth; x0 < bufferStart + stride; x0++) {
pixels[x0] = unchecked((byte) (pixels[x0] + pixels[x0 - byteDepth]));
}
// inverse delta RGBA channels of each previous stride in all but first row
for (int xy = bufferStart + stride; xy < bufferEnd; xy++) {
pixels[xy] = unchecked((byte) (pixels[xy] + pixels[xy - stride]));
}
}
#endregion
#region Private Static Decoding Methods
private static uint[,] InitWeightTables() {
uint[,] weightTables = new uint[4, 256];
for (uint i = 0; i < 256; i++) {
// Creates pattern 0x03030303, where the masked bits
// are like a stretched binary number in relation to i
uint val = ((i & 0xc0) << 18) | ((i & 0x30) << 12) |
((i & 0x0c) << 6) | ((i & 0x03));
weightTables[3, i] = val;
weightTables[2, i] = val << 2;
weightTables[1, i] = val << 4;
weightTables[0, i] = val << 6;
}
return tables;
}
/// <summary>
/// Returns the absolute and inverse absolute value tables. byte[2, 256]
/// Index 0 is absolute, index 1 is inverse absolute
/// </summary>
private static byte[,] InitAbsoluteTables() {
byte[,] absTables = new byte[2, 256]; // [0] Forward, [1] Inverse
for (int i = 0, j = 0; i < 128; i++, j+=2) {
absTables[0, i] = unchecked((byte) j);
absTables[0, 256 - i] = unchecked((byte) (j + 1));
absTables[1, j] = unchecked((byte) i);
absTables[2, j + 1] = unchecked((byte) (256 - i));
}
return absTables;
}
#endregion
#region Static Helper Methods
private static bool HasTransparency(Bitmap source) {
Rectangle bounds = new Rectangle(0, 0, source.Width, source.Height);
BitmapData data = source.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try {
return HgxImage.HasTransparency(data, bounds);
} finally {
source.UnlockBits(data);
}
}
private static unsafe bool HasTransparency(BitmapData data, Rectangle bounds) {
byte* pixels = (byte*) data.Scan0.ToPointer();
//bool transparency = false;
for (int y = bounds.Top; y < bounds.Bottom; y++) {
for (int x = bounds.Left; x < bounds.Right; x++) {
byte alpha = pixels[y * data.Stride + 4 * x + 3];
if (alpha != 255) {
return true;
}
}
}
return false;
}
private static Rectangle GetBoundingBox(Bitmap source, out bool transparency) {
Rectangle bounds = new Rectangle(0, 0, source.Width, source.Height);
BitmapData data = source.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try {
bounds = HgxImage.GetBoundingBox(data);
transparency = HgxImage.HasTransparency(data, bounds);
} finally {
source.UnlockBits(data);
}
}
private static unsafe Rectangle GetBoundingBox(BitmapData data) {
byte* pixels = (byte*) data.Scan0.ToPointer();
int left = 0, top = 0;
int right = data.Width, bottom = data.Height;
// find left border
bool stop = false;
for (int x = 0; x < data.Width && !stop; x++) {
for (int y = 0; y < data.Height; y++) {
byte alpha = pixels[y * data.Stride + 4 * x + 3];
if (alpha != 0) {
left = x;
stop = true;
break;
}
}
}
// non-transparent pixel not found, image is empty
if (!stop)
return Rectangle.Empty;
// find top border
stop = false;
for (int y = 0; y < data.Height && !stop; y++) {
for (int x = left; x < data.Width; x++) {
byte alpha = pixels[y * data.Stride + 4 * x + 3];
if (alpha != 0) {
top = y;
stop = true;
break;
}
}
}
// find right border
stop = false;
for (int x = data.Width - 1; x >= left && !stop; x--) {
for (int y = top; y < data.Height; y++) {
byte alpha = pixels[y * data.Stride + 4 * x + 3];
if (alpha != 0) {
right = x + 1;
stop = true;
break;
}
}
}
// find bottom border
stop = false;
for (int y = data.Height - 1; y >= top && !stop; y--) {
for (int x = left; x < right; x++) {
byte alpha = pixels[y * data.Stride + 4 * x + 3];
if (alpha != 0) {
bottom = y + 1;
stop = true;
break;
}
}
}
return Rectangle.FromLTRB(left, top, right, bottom);
}
#endregion
#region Private Classes
private struct BitBuffer {
#region Fields
private byte[] _buffer; // Entire buffer
private int _bit; // Bit offset in working byte
private int _index; // Byte offset of working byte
private int _remaining; // Remaining buffer bytes (including working byte)
#endregion
#region Constructors
public BitBuffer(byte[] buffer, int length) {
if (buffer == null)
throw new ArgumentException(nameof(buffer));
if (length < 0)
throw new ArgumentOutOfRangeException(nameof(length), length, "Length is less than zero");
if (length > buffer.Length)
throw new ArgumentOutOfRangeException(nameof(length), length, "Length is greater than Buffer length");
this._buffer = buffer;
this._bit = 8; // set to check for EOF on next bit
this._index = -1; // Incremented on next bit
this._remaining = length + 1; // Decremented on next bit
}
#endregion
#region Read Methods
public bool ReadFlag() {
if (this._bit > 7) {
this._bit = 0;
this._index++;
if (--this._remaining <= 0)
throw new IOException("Unexpected end of bit buffer reached");
}
return ((this._buffer[this._index] >> this._bit++) & 0x1) != 0;
}
public int ReadEliasGamma() {
int digits = 0;
while (!this.ReadFlag())
digits++;
int value = 1 << digits;
while (--digits >= 0) {
if (this.ReadFlag())
value |= 1 << digits;
}
return value;
}
#endregion
#region Write Methods
public void WriteFlag(bool flag) {
if (this._bit > 7) {
this._bit = 0;
this._index++;
if (--this._remaining <= 0)
throw new IOException("Unexpected end of bit buffer reached");
}
if (flag) this._buffer[this._index] |= unchecked((byte) (1 << this._bit++));
}
public void WriteEliasGamma(int value) {
int digits = 0;
while ((value >> (digits + 1)) != 0) {
digits++;
this.WriteFlag(false); // More digits
}
this.WriteFlag(true); // End of digits
while (--digits >= 0) {
this.WriteFlag(((value >> digits) & 0x1) != 0 ? true : false);
}
}
#endregion
#region Static Methods
public static int MeasureEliasGamma(int value) {
int digits = 0;
while ((value >> (digits + 1)) != 0)
digits++;
// 1 bit minimum plus 2 bits per digit
return digits * 2 + 1;
}
#endregion
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment