Skip to content

Instantly share code, notes, and snippets.

@GMMan
Last active December 5, 2020 04:26
Show Gist options
  • Save GMMan/83ce810dfb925d31fb2527a13cd11542 to your computer and use it in GitHub Desktop.
Save GMMan/83ce810dfb925d31fb2527a13cd11542 to your computer and use it in GitHub Desktop.
Game & Watch Mario Drawing Song animation converter
using System;
using System.IO;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace DrawingSongConv
{
class Program
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Usage: DrawingSongConv <decrypted_flash_path> <output_path>");
return;
}
int dataOffset = 0x12d44;
using BufferedReadStream ifs = new BufferedReadStream(new Configuration(), File.OpenRead(args[0]));
using FileStream ofs = File.Create(args[1]);
BinaryReader br = new BinaryReader(ifs);
BinaryWriter bw = new BinaryWriter(ofs);
ifs.Seek(dataOffset, SeekOrigin.Begin);
// Header
string signature = new string(br.ReadChars(3));
if (signature != "GIF") throw new InvalidDataException("Not a GIF");
bw.Write(signature.ToCharArray());
string version = new string(br.ReadChars(3));
if (version != "89a") throw new InvalidDataException("Not GIF 89a");
bw.Write(version.ToCharArray());
// Logical screen descriptor
ushort width = br.ReadUInt16();
ushort height = br.ReadUInt16();
byte packed = br.ReadByte();
byte bgIndex = br.ReadByte();
byte pixAR = br.ReadByte();
bw.Write(width);
bw.Write(height);
bw.Write(packed);
bw.Write(bgIndex);
bw.Write(pixAR);
// Global color table
if ((packed & 0x80) == 0) throw new InvalidDataException("No GCT present");
int colorDepth = (packed & 3) + 1;
int gctSize = 1 << ((packed & 3) + 1);
byte[] gct = br.ReadBytes(gctSize * 3);
bw.Write(gct);
// Frames
byte minCodeSize = br.ReadByte();
int frame = 0;
var memoryAllocator = new ArrayPoolMemoryAllocator();
using Buffer2D<byte> buffer = memoryAllocator.Allocate2D<byte>(new Size(width, height));
int peek;
while ((peek = br.PeekChar()) >= 0 && peek != 0x3b)
{
// Decode frame
// We want to also find a color to use for transparency
bool[] isColorUsed = new bool[gctSize];
using Buffer2D<byte> currFrame = memoryAllocator.Allocate2D<byte>(new Size(width, height));
using (LzwDecoder decoder = new LzwDecoder(memoryAllocator, ifs))
decoder.DecodePixels(minCodeSize, currFrame);
bool isIncremental = frame > 0;
// Update current drawing buffer and flip color
for (int y = 0; y < buffer.Height; ++y)
for (int x = 0; x < buffer.Width; ++x)
{
var pix = currFrame[x, y];
if (pix != 0)
{
pix ^= buffer[x, y];
currFrame[x, y] = pix;
buffer[x, y] = pix;
isColorUsed[pix] = true;
}
else
{
if (isIncremental) currFrame[x, y] = 0xff;
}
}
// Find first transparent color
byte transparentColor = 0xff;
if (isIncremental)
{
for (int i = 0; i < isColorUsed.Length; ++i)
{
if (!isColorUsed[i])
{
transparentColor = (byte)i;
break;
}
}
if (transparentColor == 0xff)
throw new Exception($"Cannot find unused color for transparency on frame {frame}");
// Set transparency color
for (int y = 0; y < buffer.Height; ++y)
for (int x = 0; x < buffer.Width; ++x)
{
if (currFrame[x, y] == 0xff)
currFrame[x, y] = transparentColor;
}
}
// Graphic control extension
bw.Write((byte)0x21);
bw.Write((byte)0xf9);
bw.Write((byte)4);
bw.Write((byte)((1 << 2) | (isIncremental ? 1 : 0))); // do not dispose, transparent index specified (if not first frame)
bw.Write((ushort)(1f / 60 * 5 * 100)); // 60fps, flip every 5 frames
bw.Write(transparentColor);
bw.Write((byte)0);
// Image descriptor
bw.Write((byte)0x2c);
bw.Write((ushort)0); // x
bw.Write((ushort)0); // y
bw.Write(width);
bw.Write(height);
bw.Write((byte)0); // no flags
// Image data
using LzwEncoder encoder = new LzwEncoder(memoryAllocator, colorDepth);
encoder.Encode(currFrame, ofs);
byte terminator = br.ReadByte();
if (terminator != 0) throw new InvalidDataException($"Frame terminator has unexpected value, frame {frame}");
++frame;
}
byte trailer = br.ReadByte();
if (trailer != 0x3b) throw new InvalidDataException("Invalid GIF trailer");
bw.Write(trailer);
}
}
}
diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
index 9eaa55566..a0e397c92 100644
--- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
@@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Decompresses and decodes data using the dynamic LZW algorithms.
/// </summary>
- internal sealed class LzwDecoder : IDisposable
+ public sealed class LzwDecoder : IDisposable
{
/// <summary>
/// The max decoder pixel stack size.
diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
index 195a84a1d..c20d0a7c7 100644
--- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs
@@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Joe Orost (decvax!vax135!petsd!joe)
/// </para>
/// </remarks>
- internal sealed class LzwEncoder : IDisposable
+ public sealed class LzwEncoder : IDisposable
{
/// <summary>
/// 80% occupancy
diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs
index acba3eff0..0c0b2ad91 100644
--- a/src/ImageSharp/IO/BufferedReadStream.cs
+++ b/src/ImageSharp/IO/BufferedReadStream.cs
@@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.IO
/// A readonly stream that add a secondary level buffer in addition to native stream
/// buffered reading to reduce the overhead of small incremental reads.
/// </summary>
- internal sealed class BufferedReadStream : Stream
+ public sealed class BufferedReadStream : Stream
{
private readonly int maxBufferIndex;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment