Created
May 17, 2021 22:16
-
-
Save trigger-segfault/1476a7a131adf7474144de89092d4d71 to your computer and use it in GitHub Desktop.
Rokucho Encoder for both RC8|RCT formats (untested)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Refer to here for more information and listing of Constants by Image Type | |
| // <https://github.com/AtomCrafty/MajiroTools/wiki/Format:-Rct-image> | |
| // Encoder implantation based off GARbro's: | |
| // <https://github.com/morkt/GARbro/blob/master/ArcFormats/Majiro/ImageRCT.cs#L470> | |
| // Encoding type: LZ77 | |
| // <https://en.wikipedia.org/wiki/LZ77_and_LZ78#LZ77> | |
| // Metadata: | |
| public int Width { get; } | |
| public int Height { get; } | |
| // Readonly and State: | |
| byte[] pixels; | |
| int[] shiftTable; | |
| int thunkSize; // Size in pixels of next data to write directly | |
| // RC8|RCT Constants: //|RC8 |RCT | |
| int ByteDepth { get; } // 1 3 | |
| sbyte[] BaseTable { get; } // [16] [32] | |
| int CmdShiftBit { get; } // 3 2 | |
| int CmdCountMask { get; } // 0x7 0x3 | |
| int CmdCountBase { get; } // 3 1 | |
| const int MaxThunkSize = 0xffff + 0x7f; | |
| const int MaxMatchSize = 0xffff; | |
| struct ChunkPosition { | |
| public ushort Shift; // Shift table index | |
| public ushort Length; // Length in pixels | |
| } | |
| // Pack pixels and write to stream. Expects pixels[] as: | |
| // * 8-bit indexed | BGR24 (pixel format) | |
| // * No stride padding (4-byte alignment) | |
| // Returns size of written data. | |
| public uint Pack(BinaryWriter writer, byte[] pixels) { | |
| this.shiftTable = InitShiftTable(BaseTable); | |
| this.pixels = pixels; | |
| this.thunkSize = 0; | |
| long dataOffset = writer.BaseStream.Position; | |
| // Start by writing 1 pixel | |
| writer.Write(pixels, 0, ByteDepth); | |
| int size = Width * Height; // total number of pixels (const) | |
| int pos = 1; // Current position in pixels | |
| while(pos < size) { | |
| var chunk = FindLongest(pos, size); | |
| if(chunk.Length >= CmdCountBase) { | |
| Flush(writer, pos); | |
| WriteCopyCode(writer, chunk); | |
| pos += chunk.Length; | |
| } | |
| else { | |
| if(thunkSize == MaxThunkSize) | |
| Flush(writer, pos); | |
| thunkSize++ | |
| pos++; | |
| } | |
| } | |
| Flush(writer, pos); | |
| long dataSize = writer.BaseStream.Position - dataOffset; | |
| return (uint)dataSize; | |
| } | |
| // Flush/Write thunk (read mode pixels) | |
| void Flush(BinaryWriter writer, int pos) { | |
| if(thunkSize == 0) | |
| return; // Nothing to flush | |
| // Write data code (read mode pixels) | |
| // CODE: 0 nnnnnnn [eeeeeeee eeeeeeee] | |
| int count = thunkSize - 1; // Subtract base number of pixels | |
| byte code = 0x00; // Read flag (MSB:0) | |
| code |= (byte)Math.Min(count, 0x7f); // Count | |
| writer.Write((byte)code); | |
| if(count >= 0x7f) // Write extra count | |
| writer.Write((ushort)(count - 0x7f)); | |
| // Change to byte positions | |
| thunkSize *= ByteDepth; | |
| pos *= ByteDepth; | |
| writer.Write(pixels, pos - thunkSize, thunkSize); | |
| thunkSize = 0; // Reset to 0 | |
| } | |
| // Write copy code (copy mode pixels) | |
| void WriteCopyCode(BinaryWriter writer, ChunkPosition chunk) { | |
| // CODE: 1 ssss{s | n}nn [eeeeeeee eeeeeeee] | |
| int count = chunk.Length - CmdCountBase; // Subtract base number of pixels | |
| byte code = 0x80; // Copy flag (MSB:1) | |
| code |= (byte)(chunk.Shift << CmdShiftBit); // Shift | |
| code |= (byte)Math.Min(count, CmdCountMask); // Count | |
| writer.Write((byte)code); | |
| if(count >= CmdCountMask) // Write extra count | |
| writer.Write((ushort)(count - CmdCountMask)); | |
| } | |
| // Find longest match in sliding window (shift table) | |
| ChunkPosition FindLongest(int begin, int end) { | |
| end = Math.Min(begin + MaxMatchSize, end); | |
| var chunk = new ChunkPosition { Shift = 0, Length = 0 }; | |
| for(int i = 0; i < shiftTable.Length; i++) { | |
| int offset = begin + shiftTable[i]; | |
| if(offset < 0 || offset >= begin) | |
| continue; | |
| int last = Mismatch(begin, end, offset); | |
| int weight = last - offset; | |
| if(weight > chunk.Length) { | |
| chunk.Shift = (ushort)i; | |
| chunk.Length = (ushort)weight; | |
| } | |
| } | |
| return chunk; | |
| } | |
| // Find position where bytes no longer match. Returns first2's last position. | |
| int Mismatch(int first1, int last1, int first2) { | |
| // Change to byte positions | |
| first1 *= ByteDepth; | |
| last1 *= ByteDepth; | |
| first2 *= ByteDepth; | |
| while(first1 != last1 && pixels[first1] == pixels[first2]) { | |
| ++first1; | |
| ++first2; | |
| } | |
| // Change back to pixel position | |
| return first2 / ByteDepth; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment