MAG画像ローダ

唐突にPC98時代によく使われていたMAG画像形式ファイルを読み込むプログラムをC#に移植してみました。2,3個の画像でしか試してないのでバグがあるかも。

// 謝辞
// MITH(T.Saito)氏のWAB−S用マルチグラフィックローダ WMLのソースを参考にさせていただきました。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

/// <summary>
/// MAG形式画像ローダ。
/// </summary>
public static class MagFile
{
    private static readonly sbyte[] FlagX = {0, -1, -2, -4, 0, -1, 0, -1, -2, 0, -1, -2, 0, -1, -2, 0};
    private static readonly sbyte[] FlagY = {0, 0, 0, 0, -1, -1, -2, -2, -2, -4, -4, -4, -8, -8, -8, 0};

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    private unsafe struct MagFileHeader
    {
        private fixed byte mgHeader [8];
        private fixed byte mgMachine [4];
        private fixed byte mgUser [8];
    }

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    private struct MagHeader
    {
        private readonly byte mgHeader;
        private readonly byte mgMachine;
        private readonly byte mgSystem;
        public readonly byte mgScreenmode;
        public readonly ushort mgStartX;
        public readonly ushort mgStartY;
        public readonly ushort mgEndX;
        public readonly ushort mgEndY;
        public readonly uint mgFlagA_offset;
        public readonly uint mgFlagB_offset;
        public readonly uint mgFlagBSize;
        public readonly uint mgPixelD_offst;
        public readonly uint mgPixelDSize;
    }

    private static unsafe void ReadMagFileHeader(Stream stream, out MagFileHeader magFileHeader)
    {
        fixed (void* p = &magFileHeader)
        {
            var buf = new byte[sizeof (MagFileHeader)];
            stream.Read(buf, 0, buf.Length);
            Marshal.Copy(buf, 0, (IntPtr) p, buf.Length);
        }
    }

    private static unsafe void ReadMagHeader(Stream stream, out MagHeader magHeader)
    {
        fixed (void* p = &magHeader)
        {
            var buf = new byte[sizeof (MagHeader)];
            stream.Read(buf, 0, buf.Length);
            Marshal.Copy(buf, 0, (IntPtr) p, buf.Length);
        }
    }

    private static string ReadComment(Stream stream)
    {
        var bytes = new List<byte>();
        var data = stream.ReadByte();
        while (data != 0x1a)
        {
            bytes.Add((byte) data);
            data = stream.ReadByte();
        }

        return Encoding.GetEncoding(932).GetString(bytes.ToArray());
    }

    private static PixelFormat GetPixelFormat(MagHeader magHeader)
    {
        return (magHeader.mgScreenmode & 0x80) == 0x80
                   ? PixelFormat.Format8bppIndexed
                   : PixelFormat.Format4bppIndexed;
    }

    private static Bitmap CreateBitmap(MagHeader magHeader)
    {
        return new Bitmap(magHeader.mgEndX + 1, magHeader.mgEndY + 1, GetPixelFormat(magHeader));
    }

    private static IEnumerable<Color> ReadPalette(Stream stream, MagHeader magHeader)
    {
        var n = (magHeader.mgScreenmode & 0x80) == 0x80 ? 256 : 16;
        var palette = new byte[n*3];
        stream.Read(palette, 0, palette.Length);
        for (var i = 0; i < n; ++i)
        {
            var g = palette[i*3 + 0];
            var r = palette[i*3 + 1];
            var b = palette[i*3 + 2];
            yield return Color.FromArgb(0xff, r, g, b);
        }
    }

    private static void SetPalette(Bitmap bitmap, IEnumerable<Color> colors)
    {
        var i = 0;
        var colorPalette = bitmap.Palette;
        foreach (var color in colors)
            colorPalette.Entries[i++] = color;

        bitmap.Palette = colorPalette;
    }

    private static int GetStartX(MagHeader magHeader)
    {
        var n = (magHeader.mgScreenmode & 0x80) == 0x80 ? 256 : 16;
        int startx;
        if (n == 256)
            startx = magHeader.mgStartX/4*4;
        else
            startx = magHeader.mgStartX/8*8;
        return startx;
    }

    private static int GetPixelSizeX(MagHeader magHeader)
    {
        var n = (magHeader.mgScreenmode & 0x80) == 0x80 ? 256 : 16;
        int xsize;
        int startx;
        if (n == 256)
        {
            startx = magHeader.mgStartX/4*4;
            xsize = ((magHeader.mgEndX + 4)/4*4 - startx);
        }
        else
        {
            startx = magHeader.mgStartX/8*8;
            xsize = ((magHeader.mgEndX + 8)/8*8 - startx + 1)/2;
        }

        return xsize;
    }

    private static int GetFlagASize(MagHeader magHeader)
    {
        int xsize = GetPixelSizeX(magHeader);
        var x = xsize/4;
        var ysize = magHeader.mgEndY - magHeader.mgStartY + 1;
        return (x*ysize + 7)/8;
    }

    private static void ReadFlagA(Stream stream, MagHeader magHeader, long startPos, byte[] flagA)
    {
        stream.Seek(startPos + magHeader.mgFlagA_offset, SeekOrigin.Begin);
        stream.Read(flagA, 0, flagA.Length);
    }

    private static void ReadFlagB(Stream stream, ref uint left, ref uint xpoint, long startPos, byte[] flagB)
    {
        stream.Seek(startPos + xpoint, SeekOrigin.Begin);
        if (left > 0x8000)
        {
            stream.Read(flagB, 0, 0x8000);
            xpoint += 0x8000;
            left -= 0x8000;
        }
        else
        {
            stream.Read(flagB, 0, 0x8000);
            left = 0;
        }
    }

    /// <summary>
    /// ピクセル単位の読み込み。
    /// <remarks>MAGフォーマットでは2byte単位をピクセルと呼ぶ。1ピクセルには16色の場合4ドット、256色なら2ドットがパックされている。</remarks>
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="left"></param>
    /// <param name="xpoint"></param>
    /// <param name="startPos"></param>
    /// <param name="pixel"></param>
    private static void ReadMagPixel(Stream stream, ref uint left, ref uint xpoint, long startPos, ushort[] pixel)
    {
        stream.Seek(startPos + xpoint, SeekOrigin.Begin);
        var tmp = new byte[0x8000];
        if (left > 0x8000)
        {
            stream.Read(tmp, 0, 0x8000);
            xpoint += 0x8000;
            left -= 0x8000;
        }
        else
        {
            stream.Read(tmp, 0, 0x8000);
            left = 0;
        }

        unsafe
        {
            fixed (void* p = &pixel[0])
            {
                Marshal.Copy(tmp, 0, (IntPtr) p, tmp.Length);
            }
        }
    }

    /// <summary>
    /// MAG画像のロード。
    /// </summary>
    /// <param name="stream">MAG画像のストリーム。</param>
    /// <returns>ビットマップ。</returns>
    public static Bitmap Load(Stream stream)
    {
        MagFileHeader magFileHeader;
        ReadMagFileHeader(stream, out magFileHeader);

        var comment = ReadComment(stream);

        // コメントを読み飛ばした位置がMAGヘッダの開始位置。
        var startPos = stream.Position;

        // MAGヘッダ読み込み。
        MagHeader magHeader;
        ReadMagHeader(stream, out magHeader);

        // 出力用ビットマップの作成。
        var bitmap = CreateBitmap(magHeader);

        // とりあえずタグにコメントを入れる。
        bitmap.Tag = comment;

        // パレットの設定。
        var colors = ReadPalette(stream, magHeader);
        SetPalette(bitmap, colors);

        // フラグAを読み込む。
        var flagA = new byte[GetFlagASize(magHeader)];
        ReadFlagA(stream, magHeader, startPos, flagA);

        // フラグBを読み込む。
        var flagB = new byte[0x8000];
        var leftFlagB = magHeader.mgFlagBSize;
        var offsetFlagB = magHeader.mgFlagB_offset;
        ReadFlagB(stream, ref leftFlagB, ref offsetFlagB, startPos, flagB);

        // ピクセルを読み込む。
        var pixel = new ushort[0x8000/sizeof (ushort)];
        var leftPixel = magHeader.mgPixelDSize;
        var offsetPixel = magHeader.mgPixelD_offst;
        ReadMagPixel(stream, ref leftPixel, ref offsetPixel, startPos, pixel);

        // フラグA,Bからピクセルデータを復元。
        unsafe
        {
            var pixelSizeX = GetPixelSizeX(magHeader);
            var flagBmap = new byte[pixelSizeX];
            var line = new ushort[pixelSizeX*8];

            fixed (byte* p1 = &flagA[0])
            fixed (ushort* p2 = &line[0])
            fixed (byte* p3 = &flagBmap[0])
            {
                var startx = GetStartX(magHeader);
                var pFlagA = p1;
                var pline = p2;

                var p = 0;
                var w = 0;
                var ibit = 0x80;
                var curFlagA = *pFlagA++;
                var j = 15;
                for (int y = magHeader.mgStartY; y <= magHeader.mgEndY; ++y)
                {
                    var flag = new int[16];

                    ++j;
                    for (var i = 0; i < flag.Length; ++i)
                        flag[i] = FlagX[i]*2 + ((FlagY[i] + j) & 0xf)*pixelSizeX;

                    if (j == 16)
                        j = 0;

                    var pFlagBmap = p3;
                    var ftmp = flag[0];

                    var cline = pline + (ftmp/2);

                    for (var x = 0; x < pixelSizeX;)
                    {
                        if ((curFlagA & ibit) != 0)
                        {
                            if (p >= 0x8000)
                            {
                                ReadFlagB(stream, ref leftFlagB, ref offsetFlagB, startPos, flagB);
                                p = 0;
                            }

                            *pFlagBmap ^= flagB[p++];
                        }

                        ibit >>= 1;
                        if (ibit == 0)
                        {
                            ibit = 0x80;
                            curFlagA = *pFlagA++;
                        }

                        var fr = *pFlagBmap;
                        pFlagBmap++;
                        if ((fr >> 4) == 0)
                        {
                            if (w >= 0x8000)
                            {
                                ReadMagPixel(stream, ref leftPixel, ref offsetPixel, startPos, pixel);
                                w = 0;
                            }

                            line[(ftmp + x)/2] = pixel[w/2];
                            w += 2;
                        }
                        else
                        {
                            line[(ftmp + x)/2] = line[(flag[fr >> 4] + x)/2];
                        }

                        x += 2;
                        if ((fr & 0x0f) == 0)
                        {
                            if (w >= 0x8000)
                            {
                                ReadMagPixel(stream, ref leftPixel, ref offsetPixel, startPos, pixel);
                                w = 0;
                            }
                            line[(ftmp + x)/2] = pixel[w/2];
                            w += 2;
                        }
                        else
                        {
                            line[(ftmp + x)/2] = line[(flag[fr & 0xf] + x)/2];
                        }
                        x += 2;
                    }

                    // 1ライン分をビットマップに書き出す。
                    var b = new byte[pixelSizeX];
                    for (var i = 0; i < pixelSizeX/2; i++)
                    {
                        var c = cline[i];
                        b[i*2] = (byte) (c & 0xff);
                        b[i*2 + 1] = (byte) ((c >> 8) & 0xff);
                    }

                    var dst = bitmap.LockBits(new Rectangle(magHeader.mgStartX, y, bitmap.Width, 1),
                                              ImageLockMode.WriteOnly, bitmap.PixelFormat);
                    Marshal.Copy(b, magHeader.mgStartX - startx, dst.Scan0,
                                 bitmap.PixelFormat == PixelFormat.Format8bppIndexed ? bitmap.Width : bitmap.Width/2);
                    bitmap.UnlockBits(dst);
                }
            }
        }

        return bitmap;
    }

    /// <summary>
    /// MAG画像のロード。
    /// </summary>
    /// <param name="path">MAG画像ファイル名。</param>
    /// <returns>ビットマップ。</returns>
    public static Bitmap Load(string path)
    {
        using (var fl = new FileStream(path, FileMode.Open))
        {
            return Load(fl);
        }
    }
}

/* 使い方
 * 
 * var bmp = MagFile.Load(magfile);
 * 
 */