JAM Format

From ModdingWiki
Jump to navigation Jump to search
JAM Format
JAM Format.png
Format typeImage
HardwareVGA
Colour depth6-bit (VGA)
Minimum size (pixels)! unknown
Maximum size (pixels)! unknown
PaletteInternal
Plane count1
Transparent pixels?! unknown
Hitmap pixels?No
Games

The contents of a JAM file is an image compressed using a form of RLEB compression.

Data type Name Description
UINT16LE Magic 'XCOM' sentence
UINT16LE Length1 File length
UINT16LE Width Image width
UINT16LE Height Image height
UINT16LE Layout Image layout: 8 is horizontal, 9 is vertical
UINT16LE Unknown Always 8
UINT16LE Length2 Palette length (768 bytes)
BYTE[Length2] Palette Palette colors (256 colors, 6BPP)
BYTE[Length1 - Length2 - 14] Pixels Some form of RLEB compression

Encoding:

- read a byte
- if zero it signals the end of file
- if less than 0x80, form a word with next byte :
  (((current XOR 0x40) SHL 8) OR next) then repeat next color by that amount plus 1
- if less than 0x40, repeat the next color by that amount plus 1
- if greater than or equal to 0x80, read that amount minus 0x7F colors

C# example (WPF)

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WpfApplication1
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            // convert all JAMs in a directory to PNGs
            var files = Directory.GetFiles(@"..\..\", "*.jam");
            foreach (var path in files)
            {
                using (var stream = File.OpenRead(path))
                {
                    var jam = new Jam(new BinaryReader(stream));

                    var width = jam.Header.ImageWidth;
                    var height = jam.Header.ImageHeight;
                    var paletteData = jam.Header.PaletteData;
                    var paletteColors = paletteData.Length / 3;
                    var colors = new List<Color>(paletteColors);
                    for (var i = 0; i < paletteColors; i++)
                    {
                        var r = paletteData[i * 3 + 0];
                        var g = paletteData[i * 3 + 1];
                        var b = paletteData[i * 3 + 2];
                        colors.Add(Color.FromRgb(r, g, b));
                    }
                    var palette = new BitmapPalette(colors);
                    var pixels = jam.Pixels;

                    var source = BitmapSource.Create(
                        width, height, 96, 96, PixelFormats.Indexed8, palette, pixels, width);

                    var encoder = new PngBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(source));
                    using (var fileStream = File.Create(Path.ChangeExtension(path, "PNG")))
                    {
                        encoder.Save(fileStream);
                    }

                }
            }
        }
    }

    public sealed class Jam
    {
        public readonly JamHeader Header;

        public readonly byte[] Pixels;

        public Jam(BinaryReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException(nameof(reader));

            Header = new JamHeader(reader);

            var w = Header.ImageWidth;
            var h = Header.ImageHeight;
            var l = Header.ImageLayout;
            var horizontal = l == JamLayout.Horizontal;
            var pixels = new byte[w * h];
            var offset = 0;
            while (true)
            {
                var b = reader.ReadByte();
                if (b == 0) break;

                var repeat = b < 0x80;
                var count = repeat ? (b < 0x40 ? b : ((b ^ 0x40) << 8) | reader.ReadByte()) + 1 : b - 0x7f;
                var b1 = repeat ? reader.ReadByte() : (byte)0;
                for (var i = 0; i < count; i++)
                {
                    var i1 = Offset(ref offset, w, h, horizontal);
                    pixels[i1] = repeat ? b1 : reader.ReadByte();
                }
            }
            Pixels = pixels;
        }

        private static int Offset(ref int offset, int width, int height, bool horizontal)
        {
            var x = horizontal ? offset % width : offset / height;
            var y = horizontal ? offset / width : offset % height;
            var i = y * width + x;
            offset++;
            return i;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct JamHeader
    {
        public readonly byte[] Magic;
        public readonly ushort StreamLength;
        public readonly ushort ImageWidth;
        public readonly ushort ImageHeight;
        public readonly JamLayout ImageLayout;
        public readonly ushort Unknown;
        public readonly ushort PaletteLength;
        public readonly byte[] PaletteData;

        public JamHeader(BinaryReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException(nameof(reader));

            Magic = reader.ReadBytes(4);
            if (Encoding.ASCII.GetString(Magic) != "XCOM")
                throw new InvalidDataException();

            StreamLength = reader.ReadUInt16();
            if (reader.BaseStream.Length != StreamLength)
                throw new InvalidDataException();

            ImageWidth = reader.ReadUInt16();
            ImageHeight = reader.ReadUInt16();

            ImageLayout = (JamLayout)reader.ReadUInt16();
            if (!Enum.IsDefined(typeof(JamLayout), ImageLayout))
                throw new InvalidDataException();

            Unknown = reader.ReadUInt16();

            PaletteLength = reader.ReadUInt16();
            PaletteData = new byte[PaletteLength];
            for (var i = 0; i < PaletteLength; i++)
            {
                PaletteData[i] = (byte)(reader.ReadByte() * 255 / 63);
            }
        }
    }

    public enum JamLayout : ushort
    {
        Horizontal = 8,
        Vertical = 9
    }
}

Credits

Aybe for the reverse-engineering.