JAM Format
Jump to navigation
Jump to search
JAM Format
Format type | Image |
---|---|
Hardware | VGA |
Colour depth | 6-bit (VGA) |
Minimum size (pixels) | ! unknown |
Maximum size (pixels) | ! unknown |
Palette | Internal |
Plane count | 1 |
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.