// recoil.ci - RECOIL core
//
// Copyright (C) 2009-2015  Piotr Fusik and Adrian Matoga
//
// This file is part of RECOIL (Retro Computer Image Library),
// see http://recoil.sourceforge.net
//
// RECOIL is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published
// by the Free Software Foundation; either version 2 of the License,
// or (at your option) any later version.
//
// RECOIL is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with RECOIL; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

/// Platform and pixel ratio.
enum RECOILResolution
{
	Amiga1x1,
	Amstrad1x1,
	Amstrad1x2,
	AppleII1x1,
	AppleII1x2,
	Macintosh1x1,
	Xe1x1,
	Xe2x1,
	Xe4x1,
	Xe2x2,
	Xe4x2,
	Portfolio1x1,
	St1x1,
	St1x2,
	Ste1x1,
	Ste1x2,
	Falcon1x1,
	Bbc1x1,
	Bbc2x1,
	Bbc1x2,
	C161x1,
	C162x1,
	C641x1,
	C642x1,
	Msx1x1,
	Pc1x1,
	SamCoupe1x1,
	Spectrum1x1,
	Zx811x1
}

enum IffType
{
	Ilbm,
	Pbm,
	Acbm
}

enum IceFrameMode
{
	Gr0,
	Gr0Gtia9,
	Gr0Gtia10,
	Gr0Gtia11,
	Gr12,
	Gr12Gtia9,
	Gr12Gtia10,
	Gr12Gtia11
}

abstract class Stream
{
	byte[] Content;
	int ContentOffset;
	int ContentLength;

	int ReadByte()
	{
		if (ContentOffset >= ContentLength)
			return -1;
		return Content[ContentOffset++];
	}
}

class ZxpStream : Stream
{
	int ReadChar()
	{
		if (ContentOffset >= ContentLength)
			return -1;
		int c = Content[ContentOffset++];
		// ZXP is CRLF. We support LF just in case.
		if (c == '\r' && ContentOffset < ContentLength && Content[ContentOffset++] == '\n')
			return '\n';
		return c;
	}

	bool IsEof()
		return ContentOffset >= ContentLength;

	int ReadHexDigit()
	{
		// ZX-Paintbrush outputs uppercase, SCRplus outputs lowercase
		switch (ReadChar()) {
		case '0': return 0;
		case '1': return 1;
		case '2': return 2;
		case '3': return 3;
		case '4': return 4;
		case '5': return 5;
		case '6': return 6;
		case '7': return 7;
		case '8': return 8;
		case '9': return 9;
		case 'A':
		case 'a': return 10;
		case 'B':
		case 'b': return 11;
		case 'C':
		case 'c': return 12;
		case 'D':
		case 'd': return 13;
		case 'E':
		case 'e': return 14;
		case 'F':
		case 'f': return 15;
		default:
			return -1;
		}
	}
}

/// Readable in-memory stream of bits, most-significant bit first.
class BitStream : Stream
{
	int Bits; // 8 bits sliding left with a trailing 1

	BitStream()
	{
		// Make sure we fetch a byte on first read.
		// 0x80 would do just as well.
		Bits = 0;
	}

	/// Reads one bit (0 or 1).
	/// Returns -1 on end of stream.
	int ReadBit()
	{
		if ((Bits & 0x7f) == 0) {
			if (ContentOffset >= ContentLength)
				return -1;
			Bits = Content[ContentOffset++] << 1 | 1;
		}
		else
			Bits <<= 1;
		return Bits >> 8 & 1;
	}

	/// Reads the requested number of bits and returns them
	/// as an unsigned integer with the first bit read as the most significant.
	/// Returns -1 on end of stream.
	int ReadBits(int count)
	{
		int result = 0;
		while (--count >= 0) {
			int bit = ReadBit();
			if (bit < 0)
				return -1;
			result = result << 1 | bit;
		}
		return result;
	}
}

class MppPaletteStream : BitStream
{
	int Read()
	{
		int rgb;
		switch (Content[4] & 3) {
		case 0:
			// ST 512 colors
			rgb = ReadBits(9);
			// RRRGGGBBB -> 00000RRR00000GGG00000BBB
			rgb = (rgb & 0x1c0) << 10
				| (rgb & 0x38) << 5
				| (rgb & 7);
			return rgb << 5 | rgb << 2 | (rgb >> 1 & 0x030303);
		case 1:
			// STE 4096 colors
			rgb = ReadBits(12);
			// rRRRgGGGbBBB -> 0000RRRr0000GGGg0000BBBb
			rgb = (rgb & 0x700) << 9 | (rgb & 0x870) << 5
				| (rgb & 0x87) << 1 | (rgb & 8) >> 3;
			return rgb << 4 | rgb;
		case 3:
			// STE 29791 colors
			rgb = ReadBits(15);
			// xyzrRRRgGGGbBBB -> 000RRRrx000GGGgy000BBBbz
			rgb = (rgb & 0x700) << 10 | (rgb & 0x800) << 6 | (rgb & 0x4087) << 2
				| (rgb & 0x70) << 6 | (rgb & 0x2000) >> 5
				| (rgb & 8) >> 2 | (rgb & 0x1000) >> 12;
			return rgb << 3 | (rgb >> 2 & 0x070707);
		default:
			// unreachable
			return 0;
		}
	}
}

/// Readable in-memory Run-Length-Encoded stream.
/// This class contains the compression logic.
/// Subclasses must implement `ReadCommand()`
/// and are allowed to override `ReadValue()`.
abstract class RleStream : BitStream
{
	/// Block length.
	int RepeatCount;

	/// Value for an RLE block, -1 for a block of literals.
	int RepeatValue;

	RleStream()
	{
		// Start with a new block.
		RepeatCount = 0;
	}

	/// Decodes a block from the stream.
	/// Fills `RepeatCount` with the length of the block.
	/// Sets `RepeatValue` to the RLE value
	/// or -1 if the block consists of `RepeatCount` literals.
	/// Returns `false` on end of stream.
	abstract bool ReadCommand();

	virtual int ReadValue()
		return ReadByte();

	int ReadRle()
	{
		while (RepeatCount == 0) {
			if (!ReadCommand())
				return -1;
		}
		RepeatCount--;
		if (RepeatValue >= 0)
			return RepeatValue;
		return ReadValue();
	}

	/// Uncompresses `unpackedCount` bytes to `unpacked` at `unpackedOffset`.
	/// If `unpackedStride` equals one, bytes are laid out sequentially.
	/// Otherwise bytes are laid out vertically column by column,
	/// `unpackedStride` being line width.
	bool Unpack(byte[] unpacked, int unpackedOffset, int unpackedStride, int unpackedCount)
	{
		for (int offset = 0; ; ) {
			int b = ReadRle();
			if (b < 0)
				return false;
			unpacked[unpackedOffset + offset] = b;
			offset += unpackedStride;
			if (offset >= unpackedCount) {
				if (unpackedStride > 1)
					offset -= unpackedCount - 1;
				if (offset >= unpackedStride)
					return true;
			}
		}
	}

	bool UnpackC64(byte[] unpacked, int unpackedLength)
	{
		unpacked[0] = Content[0];
		unpacked[1] = Content[1];
		return Unpack(unpacked, 2, 1, unpackedLength - 2);
	}
}

class BbgStream : RleStream
{
	int ValueBits;
	int CountBits;

	/// Reads the requested number of bits and returns them
	/// as an unsigned integer with the first bit read as the least significant.
	/// Returns -1 on end of stream.
	int ReadBitsReverse(int count)
	{
		int result = 0;
		for (int i = 0; i < count; i++) {
			switch (ReadBit()) {
			case -1:
				return -1;
			case 1:
				result |= 1 << i;
				break;
			case 0:
				break;
			}
		}
		return result;
	}

	override bool ReadCommand()
	{
		switch (ReadBit()) {
		case -1:
			return false;
		case 0:
			RepeatCount = 1;
			break;
		case 1:
			RepeatCount = ReadBitsReverse(CountBits);
			if (RepeatCount <= 0)
				return false;
			break;
		}
		RepeatValue = ReadBitsReverse(ValueBits);
		return true;
	}
}

class MspStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b == 0) {
			RepeatCount = ReadByte();
			RepeatValue = ReadByte();
		}
		else {
			RepeatCount = b;
			RepeatValue = -1;
		}
		return true;
	}
}

class C64KoalaStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b == 0xfe) {
			RepeatValue = ReadByte();
			RepeatCount = ReadByte();
		}
		else {
			RepeatValue = b;
			RepeatCount = 1;
		}
		return true;
	}
}

class GoDotStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b == 0xad) {
			RepeatCount = ReadByte();
			if (RepeatCount == 0)
				RepeatCount = 256;
			RepeatValue = ReadByte();
		}
		else {
			RepeatCount = 1;
			RepeatValue = b;
		}
		return true;
	}
}

class DrpStream : RleStream
{
	int Escape;

	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b == Escape) {
			RepeatCount = ReadByte();
			RepeatValue = ReadByte();
		}
		else {
			RepeatCount = 1;
			RepeatValue = b;
		}
		return true;
	}

	static byte[] UnpackFile(byte[] content, int contentLength, string signature, byte[] unpacked, int unpackedLength)
	{
		if (contentLength > 16 && RECOIL.IsStringAt(content, 2, signature)) {
			DrpStream() rle;
			rle.Content = content;
			rle.ContentOffset = 16;
			rle.ContentLength = contentLength;
			rle.Escape = content[15];
			if (!rle.UnpackC64(unpacked, unpackedLength))
				return null;
			return unpacked;
		}
		if (contentLength != unpackedLength)
			return null;
		return content;
	}
}

class HpmStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b == 0) {
			RepeatValue = ReadByte();
			RepeatCount = ReadByte();
		}
		else {
			RepeatCount = b;
			RepeatValue = -1;
		}
		return true;
	}
}

class PgcStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b < 128) {
			RepeatCount = b;
			RepeatValue = -1;
		}
		else {
			RepeatCount = b - 128;
			RepeatValue = ReadByte();
		}
		return true;
	}
}

class CciStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b < 128) {
			RepeatCount = b + 1;
			RepeatValue = -1;
		}
		else {
			RepeatCount = b - 127;
			RepeatValue = ReadByte();
		}
		return true;
	}

	bool UnpackGr15(byte[] unpacked, int unpackedOffset)
	{
		ContentOffset += 4;
		RepeatCount = 0;
		for (int offset = 0; ; ) {
			int b = ReadRle();
			if (b < 0)
				return false;
			unpacked[unpackedOffset + offset] = b;
			offset += 80;
			if (offset >= 7680) {
				offset -= 7679;
				if (offset >= 40)
					return true;
			}
		}
	}
}

class PackBitsStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b < 128) {
			RepeatCount = b + 1;
			RepeatValue = -1;
		}
		else if (b > 128) {
			RepeatCount = 257 - b;
			RepeatValue = ReadByte();
		}
		else
			RepeatCount = 0;
		return true;
	}
}

class SpcStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b < 128) {
			RepeatCount = b + 1;
			RepeatValue = -1;
		}
		else {
			RepeatCount = 258 - b;
			RepeatValue = ReadByte();
		}
		return true;
	}
}

class SpsStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b < 128) {
			RepeatCount = b + 3;
			RepeatValue = ReadByte();
		}
		else {
			RepeatCount = b - 127;
			RepeatValue = -1;
		}
		return true;
	}
}

class Sr8Stream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		switch (b) {
		case -1:
			return false;
		case 0:
			RepeatCount = ReadByte();
			if (RepeatCount == 0)
				RepeatCount = 256;
			RepeatValue = ReadByte();
			return true;
		case 1:
		case 2:
		case 3:
		case 4:
		case 5:
		case 6:
		case 7:
		case 8:
		case 9:
		case 10:
		case 11:
		case 12:
		case 13:
		case 14:
		case 15:
			RepeatCount = b;
			RepeatValue = ReadByte();
			return true;
		default:
			RepeatCount = 1;
			RepeatValue = b;
			return true;
		}
	}
}

class PacStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b == Content[4]) {
			b = ReadByte();
			if (b < 0)
				return false;
			RepeatCount = b + 1;
			RepeatValue = Content[5];
		}
		else if (b == Content[6]) {
			RepeatValue = ReadByte();
			if (RepeatValue < 0)
				return false;
			b = ReadByte();
			if (b < 0)
				return false;
			RepeatCount = b + 1;
		}
		else {
			RepeatCount = 1;
			RepeatValue = b;
		}
		return true;
	}
}

class XlpStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		bool rle;
		if (b < 128)
			rle = false;
		else {
			b -= 128;
			rle = true;
		}
		RepeatCount = b;
		if (b >= 0x40) {
			b = ReadByte();
			if (b < 0)
				return false;
			RepeatCount = RepeatCount - 0x40 << 8 | b;
		}
		RepeatValue = rle ? ReadByte() : -1;
		return true;
	}
}

class AmstradStream : RleStream
{
	int BlockLength;

	override bool ReadCommand()
	{
		while (BlockLength <= 0) {
			if (ReadByte() != 'M' || ReadByte() != 'J' || ReadByte() != 'H')
				return false;
			int lo = ReadByte();
			if (lo < 0)
				return false;
			int hi = ReadByte();
			if (hi < 0)
				return false;
			BlockLength = hi << 8 | lo;
		}
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b == 1) {
			RepeatCount = ReadByte();
			if (RepeatCount == 0)
				RepeatCount = 256;
			RepeatValue = ReadByte();
		}
		else {
			RepeatCount = 1;
			RepeatValue = b;
		}
		BlockLength -= RepeatCount;
		return true;
	}
}

class XeKoalaStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		bool rle;
		if (b < 128)
			rle = true;
		else {
			b -= 128;
			rle = false;
		}
		if (b == 0) {
			int hi = ReadByte();
			if (hi < 0)
				return false;
			b = ReadByte();
			if (b < 0)
				return false;
			b |= hi << 8;
		}
		RepeatCount = b;
		RepeatValue = rle ? ReadByte() : -1;
		return true;
	}

	static bool UnpackRaw(int type, byte[] content, int contentOffset, int contentLength, byte[] unpacked, int unpackedLength)
	{
		XeKoalaStream() rle;
		rle.Content = content;
		rle.ContentOffset = contentOffset;
		rle.ContentLength = contentLength;
		switch (type) {
		case 0:
			if (contentLength - contentOffset != unpackedLength)
				return false;
			content.CopyTo(contentOffset, unpacked, 0, unpackedLength);
			return true;
		case 1:
			for (int x = 0; x < 40; x++) {
				for (int offset = x; offset < 80; offset += 40) {
					for (int unpackedOffset = offset; unpackedOffset < unpackedLength; unpackedOffset += 80) {
						int b = rle.ReadRle();
						if (b < 0)
							return false;
						unpacked[unpackedOffset] = b;
					}
				}
			}
			return true;
		case 2:
			return rle.Unpack(unpacked, 0, 1, unpackedLength);
		default:
			return false;
		}
	}

	static bool UnpackWrapped(byte[] content, int contentLength, byte[] unpacked, int unpackedLength)
	{
		return contentLength >= 26
			&& content[0] == 0xff && content[1] == 0x80 && content[2] == 0xc9 && content[3] == 0xc7
			&& content[4] >= 26 && content[4] < contentLength
			&& content[5] == 0
			&& content[6] == 1 && content[8] == 0x0e
			&& content[9] == 0 && content[10] == 40
			&& content[11] == 0 && content[12] == 192
			&& content[20] == 0 && content[21] == 0
			&& UnpackRaw(content[7], content, content[4] + 1, contentLength, unpacked, unpackedLength);
	}
}

class ImgStream : RleStream
{
	int PatternRepeatCount;

	ImgStream()
	{
		PatternRepeatCount = 0;
	}

	int GetLineRepeatCount()
	{
		if (RepeatCount == 0
		 && ContentOffset + 4 < ContentLength
		 && Content[ContentOffset] == 0
		 && Content[ContentOffset + 1] == 0
		 && Content[ContentOffset + 2] == 0xff) {
			ContentOffset += 4;
			return Content[ContentOffset - 1] + 1;
		}
		return 1;
	}

	override bool ReadCommand()
	{
		if (PatternRepeatCount > 1) {
			PatternRepeatCount--;
			RepeatCount = Content[6] << 8 | Content[7];
			ContentOffset -= RepeatCount;
			return true;
		}
		int b = ReadByte();
		switch (b) {
		case -1:
			return false;
		case 0:
			b = ReadByte();
			// b==0 handled only in GetLineRepeatCount()
			if (b <= 0)
				return false;
			PatternRepeatCount = b;
			RepeatCount = Content[6] << 8 | Content[7];
			RepeatValue = -1;
			return true;
		case 0x80:
			RepeatCount = ReadByte();
			if (RepeatCount < 0)
				return false;
			RepeatValue = -1;
			return true;
		default:
			RepeatCount = b & 0x7f;
			RepeatValue = b >= 0x80 ? 0xff : 0;
			return true;
		}
	}
}

class CaStream : RleStream
{
	int EscapeByte;
	int DefaultValue;

	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b != EscapeByte) {
			RepeatCount = 1;
			RepeatValue = b;
			return true;
		}
		int c = ReadByte();
		if (c < 0)
			return false;
		if (c == EscapeByte) {
			RepeatCount = 1;
			RepeatValue = c;
			return true;
		}
		b = ReadByte();
		if (b < 0)
			return false;
		switch (c) {
		case 0:
			RepeatCount = b + 1;
			RepeatValue = ReadByte();
			break;
		case 1:
			c = ReadByte();
			if (c < 0)
				return false;
			RepeatCount = (b << 8) + c + 1;
			RepeatValue = ReadByte();
			break;
		case 2:
			if (b == 0)
				RepeatCount = 32000; // end decompression
			else {
				c = ReadByte();
				if (c < 0)
					return false;
				RepeatCount = (b << 8) + c + 1;
			}
			RepeatValue = DefaultValue;
			break;
		default:
			RepeatCount = c + 1;
			RepeatValue = b;
			break;
		}
		return true;
	}

	static bool UnpackCa(byte[] content, int contentOffset, int contentLength, byte[] unpacked, int unpackedOffset)
	{
		if (contentOffset + 4 > contentLength)
			return false;
		CaStream() rle;
		rle.Content = content;
		rle.ContentOffset = contentOffset + 4;
		rle.ContentLength = contentLength;
		rle.EscapeByte = content[contentOffset];
		rle.DefaultValue = content[contentOffset + 1];
		int unpackedStep = content[contentOffset + 2] << 8 | content[contentOffset + 3];
		if (unpackedStep >= 32000)
			return false;
		if (unpackedStep == 0) {
			rle.RepeatCount = 32000;
			rle.RepeatValue = rle.DefaultValue;
			unpackedStep = 1;
		}
		byte[32000] isFilled = 0;
		for (int offset = 0; ; ) {
			int b = rle.ReadRle();
			if (b < 0)
				return false;
			unpacked[unpackedOffset + offset] = b;
			isFilled[offset] = 1;
			offset += unpackedStep;
			if (offset >= 32000) {
				for (offset = 1; isFilled[offset] != 0; offset++)
					if (offset >= unpackedStep)
						return true;
			}
		}
	}

	static bool UnpackDel(byte[] content, int contentLength, byte[] unpacked, int blocks)
	{
		int contentOffset = blocks << 2;
		if (contentOffset >= contentLength)
			return false;
		int unpackedOffset = 0;
		for (int block = 0; block < blocks; block++) {
			int nextContentOffset = contentOffset + RECOIL.Get32BigEndian(content, block << 2);
			if (!UnpackCa(content, contentOffset, nextContentOffset, unpacked, unpackedOffset))
				return false;
			contentOffset = nextContentOffset;
			unpackedOffset += 32000;
		}
		if (blocks == 2) {
			// Actually three blocks, but the length of the third block is implicit.
			if (!UnpackCa(content, contentOffset, contentLength, unpacked, unpackedOffset))
				return false;
		}
		return true;
	}
}

class RgbStream : RleStream
{
	override int ReadValue()
		return ReadBits(12);

	override bool ReadCommand()
	{
		int b = ReadBits(4);
		if (b < 0)
			return false;
		bool rle;
		if (b < 8)
			rle = true;
		else {
			b -= 8;
			rle = false;
		}
		if (b == 0) {
			b = ReadBits(4);
			if (b < 0)
				return false;
			b += 7;
		}
		if (rle) {
			RepeatValue = ReadValue();
			b++;
		}
		else
			RepeatValue = -1;
		RepeatCount = b;
		return true;
	}
}

// TNY and PCS use identical control bytes.
abstract class TnyPcsStream : RleStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b < 128) {
			if (b == 0 || b == 1) {
				if (ContentOffset + 1 >= ContentLength)
					return false;
				RepeatCount = Content[ContentOffset] << 8 | Content[ContentOffset + 1];
				ContentOffset += 2;
			}
			else
				RepeatCount = b;
			RepeatValue = b == 1 ? -1 : ReadValue();
		}
		else {
			RepeatCount = 256 - b;
			RepeatValue = -1;
		}
		return true;
	}
}

// In TNY values don't mix with control bytes.
class TnyStream : TnyPcsStream
{
	int ValueOffset;
	int ValueLength;

	override int ReadValue()
	{
		if (ValueOffset + 1 >= ValueLength)
			return -1;
		int value = Content[ValueOffset] << 8 | Content[ValueOffset + 1];
		ValueOffset += 2;
		return value;
	}
}

class PcsStream : TnyPcsStream
{
	bool Palette;

	override int ReadValue()
	{
		// one byte for bitmap
		if (!Palette)
			return ReadByte();

		// one word for palette
		if (ContentOffset + 1 >= ContentLength)
			return -1;
		int value = Content[ContentOffset] << 8 | Content[ContentOffset + 1];
		ContentOffset += 2;
		return value;
	}

	const int UnpackedLength = 32000 + 9616 * 2;

	bool UnpackPcs(byte[] unpacked)
	{
		// bitmap
		ContentOffset += 2;
		Palette = false;
		for (int unpackedOffset = 0; unpackedOffset < 32000; unpackedOffset++) {
			int b = ReadRle();
			if (b < 0)
				return false;
			unpacked[unpackedOffset] = b;
		}
		if (RepeatCount != 0)
			return false;

		// palettes
		ContentOffset += 2;
		Palette = true;
		for (int unpackedOffset = 32000; unpackedOffset < UnpackedLength; unpackedOffset += 2) {
			int b = ReadRle();
			if (b < 0)
				return false;
			unpacked[unpackedOffset] = b >> 8;
			unpacked[unpackedOffset + 1] = b.LowByte;
		}
		// Don't check RepeatCount: "PhotoChrome files with a few extra entries have been found".
		return true;
	}
}

// Similar to TNY. Differences:
// 1. Word-sized counts belong to data, not commands.
// 2. Commands 0 and 1 are swapped.
class VdatStream : TnyStream
{
	override bool ReadCommand()
	{
		int b = ReadByte();
		if (b < 0)
			return false;
		if (b < 128) {
			if (b == 0 || b == 1) {
				RepeatCount = ReadValue();
				if (RepeatCount < 0)
					return false;
			}
			else
				RepeatCount = b;
			RepeatValue = b == 0 ? -1 : ReadValue();
		}
		else {
			RepeatCount = 256 - b;
			RepeatValue = -1;
		}
		return true;
	}
}

class HimStream : RleStream
{
	override int ReadValue()
	{
		if (ContentOffset < 18)
			return -1;
		return Content[ContentOffset--];
	}

	override bool ReadCommand()
	{
		int b = ReadValue();
		switch (b) {
		case -1:
			return false;
		case 0:
			RepeatCount = ReadValue();
			if (RepeatCount <= 0)
				return false;
			RepeatValue = ReadValue();
			return true;
		default:
			RepeatCount = b - 1;
			RepeatValue = -1;
			return true;
		}
	}
}

class Lz4Stream : Stream
{
	byte[] Unpacked;
	int UnpackedOffset;
	int UnpackedLength;

	bool Copy(int count)
	{
		if (ContentOffset + count > ContentLength
		 || UnpackedOffset + count > UnpackedLength)
			return false;
		Content.CopyTo(ContentOffset, Unpacked, UnpackedOffset, count);
		ContentOffset += count;
		UnpackedOffset += count;
		return true;
	}

	int ReadCount(int count)
	{
		if (count == 15) {
			int b;
			do {
				b = ReadByte();
				if (b < 0)
					return -1;
				count += b;
			} while (b == 255);
		}
		return count;
	}
}

// FlashPack 2.1
class A4rStream : Stream
{
	int OuterFlags;
	int InnerFlags;

	A4rStream()
	{
		OuterFlags = InnerFlags = 0;
	}

	int ReadFlag()
	{
		if ((InnerFlags & 0x7f) == 0) {
			if ((OuterFlags & 0x7f) == 0) {
				if (ContentOffset >= ContentLength)
					return -1;
				OuterFlags = Content[ContentOffset++] << 1 | 1;
			}
			else
				OuterFlags <<= 1;
			if ((OuterFlags & 0x100) == 0)
				InnerFlags = 1;
			else {
				if (ContentOffset >= ContentLength)
					return -1;
				InnerFlags = Content[ContentOffset++] << 1 | 1;
			}
		}
		else
			InnerFlags <<= 1;
		return InnerFlags >> 8 & 1;
	}

	const int MinAddress = 0x4e10;
	const int MaxAddress = 0x79ff;
	const int UnpackedLength = MaxAddress - MinAddress + 1;

	byte[UnpackedLength] Unpacked;
	int UnpackedOffset;

	bool StoreByte(int value)
	{
		if (UnpackedOffset < 0 || UnpackedOffset >= UnpackedLength)
			return false;
		Unpacked[UnpackedOffset++] = value;
		return true;
	}

	bool CopyBlock(int distance, int length)
	{
		if (distance > UnpackedOffset)
			return false;
		do {
			if (!StoreByte(Unpacked[UnpackedOffset - distance]))
				return false;
		} while (--length > 0);
		return true;
	}

	bool UnpackA4r()
	{
		Unpacked.Clear();
		UnpackedOffset = -1;
		for (;;) {
			int b;
			switch (ReadFlag()) {
			case -1:
				return false;
			case 0:
				// literal byte
				b = ReadByte();
				if (b < 0)
					return false;
				if (!StoreByte(b))
					return false;
				break;
			case 1:
				b = ReadByte();
				switch (b) {
				case -1:
					return false;
				case 0:
					// new address, first byte literal
					if (ContentOffset + 2 >= ContentLength)
						return false;
					b = ReadByte();
					UnpackedOffset = b + (ReadByte() << 8) + 0x80 - MinAddress;
					if (!StoreByte(ReadByte()))
						return false;
					break;
				case 1:
					b = ReadByte();
					switch (b) {
					case -1:
						return false;
					case 0:
						// end of stream
						return true;
					default:
						// RLE
						if (!CopyBlock(1, b + 2))
							return false;
						break;
					}
					break;
				default:
					// 2- or 3-byte LZ77
					if (!CopyBlock(0x80 - (b >> 1), 2 + (b & 1)))
						return false;
					break;
				}
				break;
			}
		}
	}
}

class FanoTree
{
	/// Count[n] == number of codes of bit length n.
	int[16] Count;
	/// Values sorted by code length.
	byte[256] Values;

	void Create(byte[] content, int contentOffset, int codeCount)
	{
		Count.Clear();
		for (int code = 0; code < codeCount; code++) {
			int bits = content[contentOffset + (code >> 1)];
			bits = (code & 1) == 0 ? bits >> 4 : bits & 0xf;
			Count[bits]++;
		}
		int[16] positions;
		int position = 0;
		for (int bits = 0; bits < 16; bits++) {
			positions[bits] = position;
			position += Count[bits];
		}
		for (int code = 0; code < codeCount; code++) {
			int bits = content[contentOffset + (code >> 1)];
			bits = (code & 1) == 0 ? bits >> 4 : bits & 0xf;
			Values[positions[bits]++] = code;
		}
	}

	int ReadCode(BitStream bitStream)
	{
		int code = 0;
		int valuesOffset = Count[0];
		for (int bits = 1; bits < 16; bits++) {
			int bit = bitStream.ReadBit();
			if (bit < 0)
				return -1;
			code = code << 1 | bit;
			int count = Count[bits];
			if (code < count)
				return Values[valuesOffset + code];
			code -= count;
			valuesOffset += count;
		}
		return -1;
	}
}

class BlazingPaddlesBoundingBox
{
	int Left;
	int Top;
	int Right;
	int Bottom;

	bool Calculate(byte[] content, int contentLength, int index, int startAddress)
	{
		if (index * 2 + 1 >= contentLength)
			return false;
		int contentOffset = content[index * 2] + (content[index * 2 + 1] << 8) - startAddress;
		if (contentOffset < 0)
			return false;
		Left = Top = Right = Bottom = 0;
		int x = 0;
		int y = 0;
		while (contentOffset < contentLength) {
			int control = content[contentOffset++];
			if (control == 0x08)
				return true;
			// bits 7-4: length-1
			int len = (control >> 4) + 1;
			// bits 1-0: direction
			switch (control & 3) {
			case 0: // right
				x += len;
				if (Right < x)
					Right = x;
				break;
			case 1: // left
				x -= len;
				if (Left > x)
					Left = x;
				break;
			case 2: // up
				y -= len;
				if (Top > y)
					Top = y;
				break;
			case 3: // down
				y += len;
				if (Bottom < y)
					Bottom = y;
				break;
			}
		}
		return false;
	}
}

class IcnParser : Stream
{
	bool SkipWhitespace()
	{
		bool got = false;
		while (ContentOffset < ContentLength) {
			switch (Content[ContentOffset]) {
			case ' ':
			case '\t':
			case '\r':
			case '\n':
				ContentOffset++;
				got = true;
				break;
			case '/':
				if (ContentOffset + 3 >= ContentLength || Content[ContentOffset + 1] != '*')
					return false;
				ContentOffset += 3;
				do {
					if (++ContentOffset > ContentLength)
						return false; // error: unclosed comment
				} while (Content[ContentOffset - 2] != '*' || Content[ContentOffset - 1] != '/');
				got = true;
				break;
			default:
				return got;
			}
		}
		return true;
	}

	bool Expect(string s)
	{
		if (!SkipWhitespace())
			return false;
		int length = s.Length;
		for (int i = 0; i < length; i++)
			if (Content[ContentOffset++] != s[i])
				return false;
		return true;
	}

	int ParseHex()
	{
		if (!Expect("0x"))
			return -1;
		int r = 0;
		for (; ContentOffset < ContentLength; ContentOffset++) {
			int c = Content[ContentOffset];
			if (c >= '0' && c <= '9')
				c -= '0';
			else if (c >= 'A' && c <= 'F')
				c -= 'A' - 10;
			else if (c >= 'a' && c <= 'f')
				c -= 'a' - 10;
			else
				return r;
			if (r >> 12 != 0)
				return -1; // 16-bit overflow
			r = r << 4 | c;
		}
		return r;
	}

	int ParseDefine(string s)
	{
		if (!Expect("#define") || !Expect(s))
			return -1;
		return ParseHex();
	}
}

// Interpreter of a subset of ZX81 Sinclair BASIC.
// Typical *.p picture program looks like this:
// 10 FAST
// 20 CLS
// 30 CLEAR
// 40 PRINT "<256 characters>";
// 50 PRINT "<256 characters>";
// 60 PRINT "<192 characters>";
// 70 LET A$="<64 characters>"
// 80 LET S=3+PEEK 16400+256*PEEK 16401
// 90 LET D=727+PEEK 16396+256*PEEK 16397
// 100 IF LEN A$<>64 THEN STOP
// 110 FOR F=0 TO 63
// 120 POKE D+F+(F>31),PEEK(S+F)
// 130 NEXT F
// 140 SLOW
// 150 PAUSE 4E4
// FAST disables the display so that the program runs faster. SLOW enables it.
// CLS clears the screen. CLEAR undefines all variables.
// Semicolon at the end of PRINT means "no new line".
// Lines 70-130 fill the bottom two rows which PRINT cannot access.
class PInterpreter : Stream
{
	byte[32 * 24] Screen;
	int ScreenOffset;
	int BottomOffset;

	const int LetS = 1; // LET S=3+PEEK 16400+256*PEEK 16401
	const int LetD = 2; // LET D=727+PEEK 16396+256*PEEK 16397
	const int ForF = 4; // FOR F=0 TO 63
	const int PokeD = 8; // POKE D+F+(F>31),PEEK(S+F)
	int BottomCode;

	int ReadNumber()
	{
		for (;;) {
			switch (ReadByte()) {
			case 0x15: // +
			case 0x16: // -
			case 0x1b: // .
			case 0x1c: // 0
			case 0x1d: // 1
			case 0x1e: // 2
			case 0x1f: // 3
			case 0x20: // 4
			case 0x21: // 5
			case 0x22: // 6
			case 0x23: // 7
			case 0x24: // 8
			case 0x25: // 9
			case 0x2a: // E
				break;
			case 0x7e:
				if (ContentOffset + 5 > ContentLength)
					return -1;
				int exp = ReadByte();
				int m0 = ReadByte();
				int m1 = ReadByte();
				ContentOffset += 2;
				if (exp > 0x90 // too big to fit in 16 bits
				 || m0 >= 0x80) // negative
					return -1; 
				if (exp <= 0x80) // between 0 and 1
					return 0;
				return ((m0 | 0x80) << 8 | m1) >> (0x90 - exp);
			default:
				return -1;
			}
		}
	}

	int PrintString(int offset)
	{
		for (;;) {
			if (offset >= ContentLength)
				return -1;
			int c = Content[offset++];
			if (c == 0x0b) // "
				break;
			if (ScreenOffset >= 24 * 32)
				return -1;
			if (c == 0xc0) // escaped "
				c = 0x0b;
			else if ((c & 0x7f) >= 0x40)
				return -1;
			Screen[ScreenOffset++] = c;
		}
		return offset;
	}

	bool Print()
	{
		for (;;) {
			switch (ReadByte()) {
			case 0x0b: // "
				ContentOffset = PrintString(ContentOffset);
				if (ContentOffset < 0)
					return false;
				break;
			case 0xc1: // AT
				int row = ReadNumber();
				if (row < 0 || row > 21 || ReadByte() != 0x1a) // ,
					return false;
				int column = ReadNumber();
				if (column < 0 || column > 31)
					return false;
				ScreenOffset = row << 5 | column;
				break;
			case 0x00: // space
			case 0x19: // ;
				break;
			case 0x76:
				ContentOffset--;
				if (Content[ContentOffset - 1] != 0x19) {
					// new line
					ScreenOffset = (ScreenOffset & ~0x1f) + 32;
				}
				return true;
			default:
				return false;
			}
		}
	}

	bool DPeek(int expectedX, int expectedAddress)
	{
		return ReadByte() == 0x14 // =
			&& ReadNumber() == expectedX
			&& ReadByte() == 0x15 // +
			&& ReadByte() == 0xd3 // PEEK
			&& ReadNumber() == expectedAddress
			&& ReadByte() == 0x15 // +
			&& ReadNumber() == 256
			&& ReadByte() == 0x17 // *
			&& ReadByte() == 0xd3 // PEEK
			&& ReadNumber() == expectedAddress + 1;
	}

	bool Let()
	{
		switch (ReadByte()) {
		case 0x26: // A
			if (ReadByte() != 0x0d || ReadByte() != 0x14 || ReadByte() != 0x0b) // $ = "
				return false;
			BottomOffset = ContentOffset;
			for (;;) {
				switch (ReadByte()) {
				case -1:
					return false;
				case 0x0b:
					return true;
				default:
					break;
				}
			}
		case 0x38: // S
			BottomCode |= LetS;
			return DPeek(3, 16400);
		case 0x29: // D
			BottomCode |= LetD;
			return DPeek(727, 16396);
		default:
			return false;
		}
	}

	bool DoIf()
	{
		return ReadByte() == 0xc6 // LEN
			&& ReadByte() == 0x26 // A
			&& ReadByte() == 0x0d // $
			&& ReadByte() == 0xdd // <>
			&& ReadNumber() == 64
			&& ReadByte() == 0xde // THEN
			&& ReadByte() == 0xe3; // STOP
	}

	bool DoFor()
	{
		BottomCode |= ForF;
		return ReadByte() == 0x2b // F
			&& ReadByte() == 0x14 // =
			&& ReadNumber() == 0
			&& ReadByte() == 0xdf // TO
			&& ReadNumber() == 63;
	}

	bool Poke()
	{
		BottomCode |= PokeD;
		return ReadByte() == 0x29 // D
			&& ReadByte() == 0x15 // +
			&& ReadByte() == 0x2b // F
			&& ReadByte() == 0x15 // +
			&& ReadByte() == 0x10 // (
			&& ReadByte() == 0x2b // F
			&& ReadByte() == 0x12 // >
			&& ReadNumber() == 31
			&& ReadByte() == 0x11 // )
			&& ReadByte() == 0x1a // ,
			&& ReadByte() == 0xd3 // PEEK
			&& ReadByte() == 0x10 // (
			&& ReadByte() == 0x38 // S
			&& ReadByte() == 0x15 // +
			&& ReadByte() == 0x2b // F
			&& ReadByte() == 0x11; // )
	}

	bool Next()
	{
		if (ReadByte() == 0x2b // F
		 && BottomOffset > 0
		 && BottomCode == (LetS | LetD | ForF | PokeD)) {
			ScreenOffset = 22 * 32;
			return PrintString(BottomOffset) >= 0;
		}
		return false;
	}

	bool Run()
	{
		ContentOffset = 0x74;
		Screen.Clear();
		ScreenOffset = 0;
		BottomOffset = -1;
		BottomCode = 0;

		for (;;) {
			if (ContentOffset + 8 > ContentLength)
				return false;
			if (ReadByte() == 0x76)
				return true; // no more lines
			ContentOffset += 3;
			switch (ReadByte()) {
			case 0xe4: // SLOW
			case 0xe5: // FAST
			case 0xfb: // CLS
			case 0xfd: // CLEAR
				break;
			case 0xe3: // STOP
			case 0xec: // GOTO
			case 0xf2: // PAUSE
				return true;
			case 0xf5: // PRINT
				if (!Print())
					return false;
				break;
			case 0xf1: // LET
				if (!Let())
					return false;
				break;
			case 0xfa: // IF
				if (!DoIf())
					return false;
				break;
			case 0xeb: // FOR
				if (!DoFor())
					return false;
				break;
			case 0xf4: // POKE
				if (!Poke())
					return false;
				break;
			case 0xf3: // NEXT
				if (!Next())
					return false;
				break;
			default:
				return false;
			}
			if (ReadByte() != 0x76)
				return false;
		}
	}
}

public class RECOIL
{
	/// RECOIL version - major part.
	public const int VersionMajor = 3;
	/// RECOIL version - minor part.
	public const int VersionMinor = 2;
	/// RECOIL version - micro part.
	public const int VersionMicro = 0;
	/// RECOIL version as a string.
	public const string Version = VersionMajor + "." + VersionMinor + "." + VersionMicro;

	/// Years RECOIL was created in.
	public const string Years = "2009-2015";

	/// Short credits for RECOIL.
	public const string Credits = "Retro Computer Image Library (C) 2009-2015 Piotr Fusik and Adrian Matoga\n";

	/// Short license notice.
	/// Display after the credits.
	public const string Copyright = 
		"This program is free software; you can redistribute it and/or modify\n" +
		"it under the terms of the GNU General Public License as published\n" +
		"by the Free Software Foundation; either version 2 of the License,\n" +
		"or (at your option) any later version.";

	public RECOIL()
	{
		SetAtari8Palette(null);
	}

	/// Maximum length of a supported input file.
	/// You may assume that files longer than this are not supported by RECOIL.
	public const int MaxContentLength = 350000;

	/// Maximum width of a decoded image.
	public const int MaxWidth = 2048;
	/// Maximum height of a decoded image.
	public const int MaxHeight = 1024;
	/// Maximum number of pixels in a decoded image.
	public const int MaxPixelsLength = 3 << 19;

	/// Decoded image width.
	int Width;
	/// Decoded image height.
	int Height;
	/// Decoded image pixels as 0xRRGGBB.
	int[MaxPixelsLength] Pixels;
	/// Platform and pixel ratio.
	RECOILResolution Resolution;
	/// Number of frames (normally 1; 2 or 3 for flickering pictures).
	int Frames;

	int LeftSkip;

	/// Returns decoded image width.
	public int GetWidth()
		return Width;

	/// Returns decoded image height.
	public int GetHeight()
		return Height;

	/// Returns pixels of the decoded image, top-down, left-to-right.
	/// Each pixel is a 24-bit integer 0xRRGGBB.
	public int[] GetPixels()
		return Pixels;

	/// Maximum length of a string returned by `GetPlatform()`.
	public const int MaxPlatformLength = "Atari Portfolio".Length;

	/// Returns the computer family of the decoded file format.
	public string GetPlatform()
	{
		switch (Resolution) {
		case RECOILResolution.Amiga1x1:
			return "Amiga";
		case RECOILResolution.Amstrad1x1:
		case RECOILResolution.Amstrad1x2:
			return "Amstrad CPC";
		case RECOILResolution.AppleII1x1:
		case RECOILResolution.AppleII1x2:
			return "Apple II";
		case RECOILResolution.Macintosh1x1:
			return "Apple Macintosh";
		case RECOILResolution.Xe1x1:
		case RECOILResolution.Xe2x1:
		case RECOILResolution.Xe4x1:
		case RECOILResolution.Xe2x2:
		case RECOILResolution.Xe4x2:
			return "Atari 8-bit";
		case RECOILResolution.Portfolio1x1:
			return "Atari Portfolio";
		case RECOILResolution.St1x1:
		case RECOILResolution.St1x2:
			return "Atari ST";
		case RECOILResolution.Ste1x1:
		case RECOILResolution.Ste1x2:
			return "Atari STE";
		case RECOILResolution.Falcon1x1:
			return "Atari Falcon";
		case RECOILResolution.Bbc1x1:
		case RECOILResolution.Bbc2x1:
		case RECOILResolution.Bbc1x2:
			return "BBC Micro";
		case RECOILResolution.C161x1:
		case RECOILResolution.C162x1:
			return "Commodore 16";
		case RECOILResolution.C641x1:
		case RECOILResolution.C642x1:
			return "Commodore 64";
		case RECOILResolution.Msx1x1:
			return "MSX";
		case RECOILResolution.Pc1x1:
			return "PC";
		case RECOILResolution.SamCoupe1x1:
			return "SAM Coupe";
		case RECOILResolution.Spectrum1x1:
			return "ZX Spectrum";
		case RECOILResolution.Zx811x1:
			return "ZX81";
		default:
			return "Unknown";
		}
	}

	/// Returns original width of the decoded image (informational).
	public int GetOriginalWidth()
	{
		switch (Resolution) {
		case RECOILResolution.Xe2x1:
		case RECOILResolution.Xe2x2:
		case RECOILResolution.Bbc2x1:
		case RECOILResolution.C162x1:
		case RECOILResolution.C642x1:
			return Width >> 1;
		case RECOILResolution.Xe4x1:
		case RECOILResolution.Xe4x2:
			return Width >> 2;
		default:
			return Width;
		}
	}

	/// Returns original height of the decoded image (informational).
	public int GetOriginalHeight()
	{
		switch (Resolution) {
		case RECOILResolution.Amstrad1x2:
		case RECOILResolution.AppleII1x2:
		case RECOILResolution.Xe2x2:
		case RECOILResolution.Xe4x2:
		case RECOILResolution.St1x2:
		case RECOILResolution.Ste1x2:
		case RECOILResolution.Bbc1x2:
			return Height >> 1;
		default:
			return Height;
		}
	}

	/// Returns the number of alternating frames the pictures is composed of.
	/// * 1 means the picture doesn't flicker.
	/// * 2 means the picture is displayed by quickly alternating two sub-pictures.
	/// * 3 means the picture is displayed by alternating three sub-pictures.
	public int GetFrames()
		return Frames;

	/// Initializes decoded image size and resolution.
	bool SetSize(int width, int height, RECOILResolution resolution)
	{
		if (width <= 0 || width > MaxWidth
		 || height <= 0 || height > MaxHeight
		 || width * height > MaxPixelsLength)
			return false;
		Width = width;
		Height = height;
		Resolution = resolution;
		Frames = 1;
		Colors = UnknownColors;
		LeftSkip = 0;
		return true;
	}

	bool SetSizeStOrFalcon(int width, int height, int bitplanes)
	{
		return SetSize(width, height, (bitplanes == 1 && width <= 640 && height <= 400)
			|| (bitplanes <= 4 && width <= 320 && height <= 200)
			? RECOILResolution.St1x1 : RECOILResolution.Falcon1x1);
	}

	/// Reads a 32-bit big endian integer from a byte array.
	static int Get32BigEndian(byte[] content, int contentOffset)
	{
		return content[contentOffset] << 24
			| content[contentOffset + 1] << 16
			| content[contentOffset + 2] << 8
			| content[contentOffset + 3];
	}

	/// Reads a 32-bit little endian integer from a byte array.
	static int Get32LittleEndian(byte[] content, int contentOffset)
	{
		return content[contentOffset]
			| content[contentOffset + 1] << 8
			| content[contentOffset + 2] << 16
			| content[contentOffset + 3] << 24;
	}

	static bool IsStringAt(byte[] content, int contentOffset, string s)
	{
		int length = s.Length;
		for (int i = 0; i < length; i++)
			if (content[contentOffset + i] != s[i])
				return false;
		return true;
	}

	bool ApplyBlend()
	{
		int pixelsLength = Width * Height;
		Frames = 2;
		for (int i = 0; i < pixelsLength; i++) {
			int rgb1 = Pixels[i];
			int rgb2 = Pixels[pixelsLength + i];
			// This clever formula just computes the byte-by-byte averages.
			Pixels[i] = (rgb1 & rgb2) + ((rgb1 ^ rgb2) >> 1 & 0x7f7f7f);
		}
		return true;
	}

	bool DecodeBru(byte[] content, int contentLength)
	{
		if (contentLength != 64)
			return false;
		SetSize(8, 8, RECOILResolution.St1x1);
		for (int i = 0; i < 64; i++) {
			switch (content[i]) {
			case 0:
				Pixels[i] = 0x000000;
				break;
			case 1:
				Pixels[i] = 0xffffff;
				break;
			default:
				return false;
			}
		}
		return true;
	}

	/// RGB palette decoded from the image file.
	int[256] ContentPalette;

	/// Decodes Atari ST/Falcon interleaved bitplanes.
	/// Each 16 pixels are encoded in N consecutive 16-bit words.
	void DecodeBitplanes(byte[] content, int contentOffset, int bitplanes, int pixelsOffset, int pixelsCount)
	{
		for (int i = 0; i < pixelsCount; i++) {
			int offset = contentOffset + (i >> 3 & ~1) * bitplanes + (i >> 3 & 1);
			int bit = ~i & 7;
			int c = 0;
			for (int bitplane = bitplanes; --bitplane >= 0; )
				c = c << 1 | content[offset + (bitplane << 1)] >> bit & 1;
			Pixels[pixelsOffset + i] = ContentPalette[c];
		}
	}

	bool DecodeBlackAndWhite(byte[] content, int contentOffset, int width, int height, int backgroundColor, RECOILResolution resolution)
	{
		if (!SetSize(width, height, resolution))
			return false;
		ContentPalette[0] = backgroundColor; // 0 or 0xffffff
		ContentPalette[1] = backgroundColor ^ 0xffffff;
		DecodeBitplanes(content, contentOffset, 1, 0, width * height);
		return true;
	}

	void DecodeBlackAndWhiteFont(byte[] content, int contentOffset, int contentLength)
	{
		for (int y = 0; y < Height; y++) {
			for (int x = 0; x < 256; x++) {
				int offset = contentOffset + ((y & ~7) << 5) + (x & ~7) + (y & 7);
				int c;
				if (offset < contentLength) {
					c = content[offset] >> (~x & 7) & 1;
					if (c != 0)
						c = 0xffffff;
				}
				else
					c = 0;
				Pixels[(y << 8) + x] = c;
			}
		}
	}

	// Atari Portfolio formats.

	bool DecodePgf(byte[] content, int contentLength)
	{
		if (contentLength != 1920)
			return false;
		return DecodeBlackAndWhite(content, 0, 240, 64, 0xffffff, RECOILResolution.Portfolio1x1);
	}

	bool DecodePgc(byte[] content, int contentLength)
	{
		if (contentLength < 33
		 || content[0] != 'P' || content[1] != 'G' || content[2] != 1)
			return false;
		PgcStream() rle;
		rle.Content = content;
		rle.ContentOffset = 3;
		rle.ContentLength = contentLength;
		byte[1920] unpacked;
		for (int unpackedOffset = 0; unpackedOffset < 1920; unpackedOffset++) {
			int b = rle.ReadRle();
			if (b < 0) {
				if (unpackedOffset == 1919) // IRAQ.PGC
					b = 0;
				else
					return false;
			}
			unpacked[unpackedOffset] = b;
		}
		return DecodePgf(unpacked, 1920);
	}

	// Apple Macintosh formats.

	bool DecodeMac(byte[] content, int contentLength)
	{
		if (contentLength < 512
		 || content[0] != 0 || content[1] != 0 || content[2] != 0 || (content[3] & ~2) != 0)
			return false;
		PackBitsStream() rle;
		rle.Content = content;
		rle.ContentOffset = 512;
		rle.ContentLength = contentLength;
		byte[51840] unpacked;
		if (!rle.Unpack(unpacked, 0, 1, 51840))
			return false;
		return DecodeBlackAndWhite(unpacked, 0, 576, 720, 0xffffff, RECOILResolution.Macintosh1x1);
	}

	// BBC micro formats.

	const int[16] BbcPalette = {
		0x000000,
		0xff0000,
		0x00ff00,
		0xffff00,
		0x0000ff,
		0xff00ff,
		0x00ffff,
		0xffffff,
		// Colors 8-15 are flashing with the negative color
		// (e.g. color 9 = color 1 alternated with color 6).
		// We don't support flashing here and just make colors 8-15 identical to 0-7.
		0x000000,
		0xff0000,
		0x00ff00,
		0xffff00,
		0x0000ff,
		0xff00ff,
		0x00ffff,
		0xffffff
	};

	const int[4] BbcPalette2Bit = {
		0x000000,
		0xff0000,
		0xffff00,
		0xffffff
	};

	const int[2] BbcPalette1Bit = {
		0x000000,
		0xffffff
	};

	bool DecodeBb0(byte[] content, int contentLength, int[] palette)
	{
		if (contentLength != 20480)
			return false;
		SetSize(640, 512, RECOILResolution.Bbc1x2);
		for (int y = 0; y < 256; y++) {
			for (int x = 0; x < 640; x++) {
				int c = content[(y & ~7) * 80 + (x & ~7) + (y & 7)] >> (~x & 7) & 1;
				int pixelsOffset = y * 1280 + x;
				Pixels[pixelsOffset + 640] = Pixels[pixelsOffset] = palette[c];
			}
		}
		return true;
	}

	bool DecodeBb1(byte[] content, int contentLength, int[] palette)
	{
		if (contentLength != 20480)
			return false;
		SetSize(320, 256, RECOILResolution.Bbc1x1);
		for (int y = 0; y < 256; y++) {
			for (int x = 0; x < 320; x++) {
				int c = content[(y & ~7) * 80 + ((x & ~3) << 1) + (y & 7)] >> (~x & 3);
				Pixels[y * 320 + x] = palette[(c >> 3 & 2) + (c & 1)];
			}
		}
		return true;
	}

	bool DecodeBb2(byte[] content, int contentLength, int[] palette)
	{
		if (contentLength != 20480)
			return false;
		SetSize(320, 256, RECOILResolution.Bbc2x1);
		for (int y = 0; y < 256; y++) {
			for (int x = 0; x < 160; x++) {
				int c = content[(y & ~7) * 80 + ((x & ~1) << 2) + (y & 7)] >> (~x & 1);
				int pixelsOffset = (y * 160 + x) << 1;
				Pixels[pixelsOffset + 1] = Pixels[pixelsOffset] = palette[(c >> 3 & 8) + (c >> 2 & 4) + (c >> 1 & 2) + (c & 1)];
			}
		}
		return true;
	}

	bool DecodeBb4(byte[] content, int contentLength, int[] palette)
	{
		if (contentLength != 10240)
			return false;
		SetSize(320, 256, RECOILResolution.Bbc1x1);
		for (int y = 0; y < 256; y++) {
			for (int x = 0; x < 320; x++) {
				int c = content[(y & ~7) * 40 + (x & ~7) + (y & 7)] >> (~x & 7) & 1;
				Pixels[y * 320 + x] = palette[c];
			}
		}
		return true;
	}

	bool DecodeBb5(byte[] content, int contentLength, int[] palette)
	{
		if (contentLength != 10240)
			return false;
		SetSize(320, 256, RECOILResolution.Bbc2x1);
		for (int y = 0; y < 256; y++) {
			for (int x = 0; x < 160; x++) {
				int c = content[(y & ~7) * 40 + ((x & ~3) << 1) + (y & 7)] >> (~x & 3);
				int pixelsOffset = (y * 160 + x) << 1;
				Pixels[pixelsOffset + 1] = Pixels[pixelsOffset] = palette[(c >> 3 & 2) + (c & 1)];
			}
		}
		return true;
	}

	bool DecodeBbg(byte[] content, int contentLength)
	{
		BbgStream() rle;
		rle.Content = content;
		rle.ContentOffset = 0;
		rle.ContentLength = contentLength;
		rle.ValueBits = rle.ReadBitsReverse(8);
		if (rle.ValueBits < 1 || rle.ValueBits > 8)
			return false;
		int mode = rle.ReadBitsReverse(8);
		int unpackedLength;
		switch (mode) {
		case 0:
		case 1:
		case 2:
			unpackedLength = 20480;
			break;
		case 4:
		case 5:
			unpackedLength = 10240;
			break;
		default:
			return false;
		}
		for (int i = 15; i >= 0; i--) {
			int c = rle.ReadBitsReverse(4);
			if (c < 0)
				return false;
			ContentPalette[i] = BbcPalette[c];
		}
		int unpackedStep = rle.ReadBitsReverse(8);
		if (unpackedStep <= 0)
			return false;
		rle.CountBits = rle.ReadBitsReverse(8);
		if (rle.CountBits < 1 || rle.CountBits > 8)
			return false;

		byte[20480] unpacked;
		for (int unpackedOffset = unpackedStep - 1;;) {
			int b = rle.ReadRle();
			if (b < 0)
				return false;
			unpacked[unpackedOffset] = b;
			unpackedOffset += unpackedStep;
			if (unpackedOffset >= unpackedLength) {
				unpackedOffset = unpackedOffset % unpackedStep - 1;
				if (unpackedOffset < 0)
					break;
			}
		}

		switch (mode) {
		case 0:
			return DecodeBb0(unpacked, unpackedLength, ContentPalette);
		case 1:
			return DecodeBb1(unpacked, unpackedLength, ContentPalette);
		case 2:
			return DecodeBb2(unpacked, unpackedLength, ContentPalette);
		case 4:
			return DecodeBb4(unpacked, unpackedLength, ContentPalette);
		case 5:
			return DecodeBb5(unpacked, unpackedLength, ContentPalette);
		default:
			return false;
		}
	}

	// Amstrad CPC formats.

	static int GetAmstradHeader(byte[] content, int contentLength)
	{
		if (contentLength < 128
		 || (content[0x18] | content[0x19] << 8) != contentLength - 128
		 || content[0x40] != content[0x18] || content[0x41] != content[0x19] || content[0x42] != 0)
			return 0;
		int sum = 0;
		for (int i = 0; i < 67; i++)
			sum += content[i];
		if ((content[0x43] | content[0x44] << 8) != sum)
			return 0;
		return 128;
	}

	bool DecodeAmstradFnt(byte[] content, int contentLength)
	{
		int contentOffset = GetAmstradHeader(content, contentLength);
		if (contentLength < contentOffset + 768)
			return false;
		SetSize(256, 24, RECOILResolution.Amstrad1x1);
		DecodeBlackAndWhiteFont(content, contentOffset, contentLength);
		return true;
	}

	bool DecodeAmstradMode2(byte[] content, int contentOffset, int width, int height)
	{
		SetSize(width, height << 1, RECOILResolution.Amstrad1x2);
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				int offset = (y * width << 1) + x;
				int c = content[contentOffset + ((y & 7) << 11) + ((y >> 3) * width + x >> 3)] >> (~x & 7) & 1;
				if (c != 0)
					c = 0xffffff;
				Pixels[offset + width] = Pixels[offset] = c;
			}
		}
		return true;
	}

	bool DecodeHgb(byte[] content, int contentLength)
	{
		int contentOffset = GetAmstradHeader(content, contentLength);
		if (contentLength != contentOffset + 16384)
			return false;
		return DecodeAmstradMode2(content, contentOffset, 512, 256);
	}

	bool DecodeAmstradScr(byte[] content, int contentLength)
	{
		int contentOffset = GetAmstradHeader(content, contentLength);
		if (contentLength == contentOffset + 16384)
			return DecodeAmstradMode2(content, contentOffset, 640, 200);
		AmstradStream() rle;
		rle.Content = content;
		rle.ContentOffset = contentOffset;
		rle.ContentLength = contentLength;
		rle.BlockLength = 0;
		byte[16384] unpacked;
		if (!rle.Unpack(unpacked, 0, 1, 16384))
			return false;
		return DecodeAmstradMode2(unpacked, 0, 640, 200);
	}

	// ZX81 formats.

	bool DecodeP(byte[] content, int contentLength)
	{
		PInterpreter() interp;
		interp.Content = content;
		interp.ContentLength = contentLength;
		if (!interp.Run())
			return false;
		SetSize(256, 192, RECOILResolution.Zx811x1);
		byte[] font = BinaryResource("zx81.fnt");
		for (int y = 0; y < 192; y++) {
			for (int x = 0; x < 256; x++) {
				int c = interp.Screen[y >> 3 << 5 | x >> 3];
				int b = font[(c & 0x3f) << 3 | (y & 7)] >> (~x & 7) & 1;
				Pixels[y << 8 | x] = b == c >> 7 ? 0xffffff : 0;
			}
		}
		return true;
	}

	// ZX Spectrum formats.

	const int ZxBitmapCheckerboard = -3;
	const int ZxBitmapHlr = -2;
	const int ZxBitmapLinear = -1;

	const int ZxAttributesNone = -3;
	const int ZxAttributesMg1 = -2;
	const int ZxAttributesTimex = -1;
	const int ZxAttributes8x1 = 0;
	const int ZxAttributes8x2 = 1;
	const int ZxAttributes8x4 = 2;
	const int ZxAttributes8x8 = 3;

	void DecodeZx(byte[] content, int bitmapOffset, int attributesOffset, int attributesMode, int pixelsOffset)
	{
		for (int y = 0; y < 192; y++) {
			for (int x = 0; x < 256; x++) {
				int col = x >> 3;
				const int[16] palette = {
					0x000000,
					0x0000cd,
					0xcd0000,
					0xcd00cd,
					0x00cd00,
					0x00cdcd,
					0xcdcd00,
					0xcdcdcd,
					0x000000,
					0x0000ff,
					0xff0000,
					0xff00ff,
					0x00ff00,
					0x00ffff,
					0xffff00,
					0xffffff
				};
				int c;
				switch (bitmapOffset) {
				case ZxBitmapCheckerboard:
					c = x ^ y;
					break;
				case ZxBitmapHlr:
					c = content[0x54 + (y & 7)] >> (~x & 7);
					break;
				case ZxBitmapLinear:
					c = content[y << 5 | col] >> (~x & 7);
					break;
				default:
					// x=CCCCCBBB, y=AARRRLLL -> o=AALLLRRRCCCCC
					c = content[bitmapOffset + ((y & 0xc0) << 5) + ((y & 7) << 8) + ((y & 0x38) << 2) + col] >> (~x & 7);
					break;
				}
				c &= 1;
				if (attributesMode == ZxAttributesNone) {
					// black and white
					if (c != 0)
						c = 0xffffff;
				}
				else {
					int a;
					switch (attributesMode) {
					case ZxAttributesMg1:
						// MG1 file format:
						// 0x0000 hdr
						// 0x0100 scr1
						// 0x1900 scr2
						// 0x3100 att1 8x1, 16 columns (8..23) * 192 rows
						// 0x3d00 att2 8x1, 16 columns (8..23) * 192 rows
						// 0x4900 att1 8x8, 16 columns (0..7,24..31) * 24 rows
						// 0x4a80 att2 8x8, 16 columns (0..7,24..31) * 24 rows
						if (col < 8)
							a = attributesOffset + (y >> 3 << 4);
						else if (col < 24)
							a = (attributesOffset == 0x4900 ? 0x3100 - 8 : 0x3d00 - 8) + (y << 4);
						else
							a = attributesOffset + (y >> 3 << 4) - 16;
						break;
					case ZxAttributesTimex:
						a = attributesOffset + ((y & 0xc0) << 5) + ((y & 7) << 8) + ((y & 0x38) << 2);
						break;
					default:
						// linear 8x1, 8x2, 8x4, 8x8
						a = attributesOffset + (y >> attributesMode << 5);
						break;
					}
					a = content[a + col];
					c = c == 0 ? a >> 3 & 0xf : (a >> 3 & 8) | (a & 7);
					c = palette[c];
				}
				Pixels[pixelsOffset + (y << 8) + x] = c;
			}
		}
	}

	bool DecodeScr(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 6144:
			SetSize(256, 192, RECOILResolution.Spectrum1x1);
			DecodeZx(content, 0, -1, ZxAttributesNone, 0);
			return true;
		case 6912:
		case 6913: // border color - ignore
			SetSize(256, 192, RECOILResolution.Spectrum1x1);
			DecodeZx(content, 0, 0x1800, ZxAttributes8x8, 0);
			return true;
		case 12288:
			SetSize(256, 192, RECOILResolution.Spectrum1x1);
			DecodeZx(content, 0, 0x1800, ZxAttributesTimex, 0);
			return true;
		default:
			return DecodeAmstradScr(content, contentLength);
		}
	}

	bool DecodeZxIfl(byte[] content, int contentLength)
	{
		if (contentLength != 9216)
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		DecodeZx(content, 0, 0x1800, ZxAttributes8x2, 0);
		return true;
	}

	bool DecodeMc(byte[] content, int contentLength)
	{
		if (contentLength != 12288)
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		DecodeZx(content, ZxBitmapLinear, 0x1800, ZxAttributes8x1, 0);
		return true;
	}

	bool DecodeZxImg(byte[] content, int contentLength)
	{
		if (contentLength != 13824)
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		DecodeZx(content, 0, 0x1800, ZxAttributes8x8, 0);
		DecodeZx(content, 0x1b00, 0x3300, ZxAttributes8x8, 256 * 192);
		return ApplyBlend();
	}

	bool DecodeMg(byte[] content, int contentLength)
	{
		if (contentLength < 14080
		 || content[0] != 'M' || content[1] != 'G' || content[2] != 'H'
		 || content[3] != 1)
			return false;
		int attributesMode;
		// this should match filename extension, but let's rely on file content
		switch (content[4]) {
		case 1:
			if (contentLength != 19456)
				return false;
			SetSize(256, 192, RECOILResolution.Spectrum1x1);
			DecodeZx(content, 0x100, 0x4900, ZxAttributesMg1, 0);
			DecodeZx(content, 0x1900, 0x4a80, ZxAttributesMg1, 256 * 192);
			return ApplyBlend();
		case 2:
			attributesMode = ZxAttributes8x2;
			break;
		case 4:
			attributesMode = ZxAttributes8x4;
			break;
		case 8:
			attributesMode = ZxAttributes8x8;
			break;
		default:
			return false;
		}
		if (contentLength != 256 + 2 * 6144 + (2 * 6144 >> attributesMode))
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		DecodeZx(content, 0x100, 0x3100, attributesMode, 0);
		DecodeZx(content, 0x1900, 0x3100 + (6144 >> attributesMode), attributesMode, 256 * 192);
		return ApplyBlend();
	}

	bool DecodeAtr(byte[] content, int contentLength)
	{
		if (contentLength != 768)
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		DecodeZx(content, ZxBitmapCheckerboard, 0, ZxAttributes8x8, 0);
		return true;
	}

	bool DecodeHlr(byte[] content, int contentLength)
	{
		// The 84-byte "header" contains code to display the picture. Here we check just a few bytes
		if (contentLength != 1628 || content[0] != 0x76 || content[1] != 0xaf || content[2] != 0xd3 || content[3] != 0xfe
		 || content[4] != 0x21 || content[5] != 0x00 || content[6] != 0x58)
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		DecodeZx(content, ZxBitmapHlr, 0x5c, ZxAttributes8x8, 0);
		DecodeZx(content, ZxBitmapHlr, 0x35c, ZxAttributes8x8, 256 * 192);
		return ApplyBlend();
	}

	bool DecodeZxRgb3(byte[] content, int contentLength, byte[] frameComponents)
	{
		if (contentLength != 18432)
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		Frames = 3;
		for (int y = 0; y < 192; y++) {
			for (int x = 0; x < 256; x++) {
				int offset = (y & 0xc0) << 5 | (y & 7) << 8 | (y & 0x38) << 2 | x >> 3;
				int c = 0;
				for (int frame = 0; frame < 3; frame++) {
					if ((content[frame * 0x1800 + offset] >> (~x & 7) & 1) != 0)
						c |= 0xff << frameComponents[frame]; // probably too bright, but 0x55 seems too dark?
				}
				Pixels[(y << 8) + x] = c;
			}
		}
		return true;
	}

	bool DecodeZxRgb(byte[] content, int contentLength)
	{
		const byte[3] frameComponents = { 16, 8, 0 }; // R, G, B
		return DecodeZxRgb3(content, contentLength, frameComponents);
	}

	bool Decode3(byte[] content, int contentLength)
	{
		const byte[3] frameComponents = { 0, 16, 8 }; // B, R, G
		return DecodeZxRgb3(content, contentLength, frameComponents);
	}

	bool DecodeCh8(byte[] content, int contentLength)
	{
		int height;
		switch (contentLength) {
		case 768:
			height = 24;
			break;
		case 2048:
			height = 64;
			break;
		default:
			return false;
		}
		SetSize(256, height, RECOILResolution.Spectrum1x1);
		DecodeBlackAndWhiteFont(content, 0, contentLength);
		return true;
	}

	bool DecodeZxp(byte[] content, int contentLength)
	{
		if (contentLength < 51889 - 219 || !IsStringAt(content, 0, "ZX-Paintbrush "))
			return false;
		int contentOffset = 14;
		if (IsStringAt(content, 14, "extended "))
			contentOffset = 23;
		if (!IsStringAt(content, contentOffset, "image"))
			return false;
		ZxpStream() s;
		s.Content = content;
		s.ContentOffset = contentOffset + 5;
		s.ContentLength = contentLength;
		if (s.ReadChar() != '\n' || s.ReadChar() != '\n')
			return false;

		byte[12288] scr;
		for (int y = 0; y < 192; y++) {
			int b = 0;
			for (int x = 0; x < 256; x++) {
				int bit = ~x & 7;
				switch (s.ReadChar()) {
				case '*':
				case '0':
					break;
				case '1':
					b |= 1 << bit;
					break;
				default:
					return false;
				}
				if (bit == 0) {
					scr[(y << 5) | (x >> 3)] = b;
					b = 0;
				}
			}
			if (s.ReadChar() != '\n')
				return false;
		}
		if (s.ReadChar() != '\n')
			return false;

		for (int y = 0; y < 192; y++) {
			if (y == 24 && s.IsEof()) {
				SetSize(256, 192, RECOILResolution.Spectrum1x1);
				DecodeZx(scr, ZxBitmapLinear, 0x1800, ZxAttributes8x8, 0);
				return true;
			}
			for (int x = 0; x < 32; x++) {
				int hi = s.ReadHexDigit();
				if (hi < 0)
					return false;
				int lo = s.ReadHexDigit();
				if (lo < 0)
					return false;
				scr[0x1800 + (y << 5) + x] = (hi << 4) + lo;
				if (s.ReadChar() != (x < 31 ? ' ' : '\n'))
					return false;
			}
		}

		if (!s.IsEof())
			return false;
		SetSize(256, 192, RECOILResolution.Spectrum1x1);
		DecodeZx(scr, ZxBitmapLinear, 0x1800, ZxAttributes8x1, 0);
		return true;
	}

	// MSX formats.

	static int GetMsxHeader(byte[] content)
	{
		if (content[1] != 0 || content[2] != 0 || content[5] != 0 || content[6] != 0)
			return -1;
		return content[3] | content[4] << 8;
	}

	static bool ExpectMsxHeader(byte[] content, int expected)
	{
		int value = GetMsxHeader(content);
		return value == expected || value == expected - 1;
	}

	bool DecodeSc2(byte[] content, int contentLength)
	{
		// TODO: 16391-byte SC2 files with sprites
		if (contentLength != 14343 || content[0] != 0xfe || !ExpectMsxHeader(content, 0x3800))
			return false;
		SetSize(256, 192, RECOILResolution.Msx1x1);
		for (int y = 0; y < 192; y++) {
			int fontOffset = 7 + ((y &0xc0) << 5);
			for (int x = 0; x < 256; x++) {
				int b = fontOffset + (content[0x1807 + ((y & ~7) << 2) + (x >> 3)] << 3) + (y & 7);
				int c = content[0x2000 + b];
				// TODO: 0xRB 0x0G, 3 bits per component palette at 0x1b87.
				// Use the following default if all zero.
				// All files I have either don't specify the palette (all zeros)
				// or specify a palette that is identical to the default one.
				const int[16] palette = {
					0x000000,
					0x000000,
					0x3eb849,
					0x74d07d,
					0x5955e0,
					0x8076f1,
					0xb95e51,
					0x65dbef,
					0xdb6559,
					0xff897d,
					0xccc35e,
					0xded087,
					0x3aa241,
					0xb766b5,
					0xcccccc,
					0xffffff
				};
				Pixels[(y << 8) + x] = palette[(content[b] >> (~x & 7) & 1) == 0 ? c & 0xf : c >> 4];
			}
		}
		return true;
	}

	bool DecodeValidSc8(byte[] content)
	{
		int[256] palette;
		for (int c = 0; c < 256; c++) {
			// 0bGGGRRRBB -> 0bRRRRRRRRGGGGGGGGBBBBBBBB
			palette[c] = (c & 0x1c) * 0x48 >> 3 << 16
				| (c >> 5) * 0x48 >> 1 << 8
				| (c & 3) * 0x55;
		}
		SetSize(256, 212, RECOILResolution.Msx1x1);
		for (int i = 0; i < 212 * 256; i++)
			Pixels[i] = palette[content[7 + i]];
		return true;
	}

	static bool ExpectMsx2(byte[] content, int contentLength)
		return contentLength >= 54279 && content[0] == 0xfe && ExpectMsxHeader(content, 0xd400);

	bool DecodeSc8(byte[] content, int contentLength)
		return ExpectMsx2(content, contentLength) && DecodeValidSc8(content);

	bool DecodeSr8(byte[] content, int contentLength)
	{
		if (contentLength < 7)
			return false;
		if (content[0] != 0xfd)
			return DecodeSc8(content, contentLength);
		if (GetMsxHeader(content) + 7 != contentLength)
			return false;
		byte[54279] unpacked;
		Sr8Stream() rle;
		rle.Content = content;
		rle.ContentOffset = 7;
		rle.ContentLength = contentLength;
		return rle.Unpack(unpacked, 7, 1, 54272) && DecodeValidSc8(unpacked);
	}

	static int DecodeMsxYjk(byte[] content, int i)
	{
		int y = content[7 + i] >> 3;
		i &= ~3;
		int k = (content[i + 7] & 7) | (content[i + 8] & 7) << 3;
		int j = (content[i + 9] & 7) | (content[i + 10] & 7) << 3;
		// to 6-bit signed
		k -= (k & 0x20) << 1;
		j -= (j & 0x20) << 1;
		// YJK to RGB
		int r = y + j;
		if (r < 0)
			r = 0;
		else if (r > 31)
			r = 31;
		int g = y + k;
		if (g < 0)
			g = 0;
		else if (g > 31)
			g = 31;
		int b = (((5 * y - k) >> 1) - j) >> 1;
		if (b < 0)
			b = 0;
		else if (b > 31)
			b = 31;
		int rgb = r << 16 | g << 8 | b;
		// 5-bit RGB to 8-bit RGB
		return rgb << 3 | (rgb >> 2 & 0x070707);
	}

	bool DecodeScc(byte[] content, int contentLength)
	{
		if (!ExpectMsx2(content, contentLength))
			return false;
		SetSize(256, 212, RECOILResolution.Msx1x1);
		for (int i = 0; i < 212 * 256; i++)
			Pixels[i] = DecodeMsxYjk(content, i);
		return true;
	}

	bool DecodeSca(byte[] content, int contentLength)
	{
		if (contentLength != 64263 || GetMsxHeader(content) < 0xd400)
			return false;
		int[16] palette;
		for (int i = 0; i < 16; i++) {
			// 0xRB 0x0G, 3 bits per component 
			int rb = content[0xfa87 + (i << 1)];
			int g = content[0xfa88 + (i << 1)];
			int rgb = (rb & 0x70) << 12 | (rb & 7) | (g & 7) << 8;
			palette[i] = rgb << 5 | rgb << 2 | (rgb >> 1 & 0x030303);
		}
		SetSize(256, 212, RECOILResolution.Msx1x1);
		for (int i = 0; i < 212 * 256; i++) {
			int c = content[7 + i];
			if ((c & 8) == 0)
				c = DecodeMsxYjk(content, i);
			else
				c = palette[c >> 4];
			Pixels[i] = c;
		}
		return true;
	}

	// Apple II formats.

	bool DecodeHgr(byte[] content, int contentLength)
	{
		if (contentLength < 8184)
			return false;
		SetSize(280, 192, RECOILResolution.AppleII1x1);
		for (int y = 0; y < 192; y++) {
			int lineOffset = (y & 7) << 10 | (y & 0x38) << 4 | (y >> 6) * 40;
			for (int x = 0; x < 280; x++) {
				int c = content[lineOffset + x / 7] >> (x % 7) & 1;
				if (c != 0)
					c = 0xffffff;
				Pixels[y * 280 + x] = c;
			}
		}
		return true;
	}

	bool DecodeDhr(byte[] content, int contentLength)
	{
		if (contentLength != 16384)
			return false;
		SetSize(560, 384, RECOILResolution.AppleII1x2);
		for (int y = 0; y < 192; y++) {
			int lineOffset = (y & 7) << 10 | (y & 0x38) << 4 | (y >> 6) * 40;
			for (int x = 0; x < 560; x++) {
				int b = x / 7;
				int c = content[((b & 1) << 13) + lineOffset + (b >> 1)] >> (x % 7) & 1;
				if (c != 0)
					c = 0xffffff;
				int pixelsOffset = y * (560 * 2) + x;
				Pixels[pixelsOffset + 560] = Pixels[pixelsOffset] = c;
			}
		}
		return true;
	}

	bool DecodeShr(byte[] content, int contentLength)
	{
		if (contentLength != 32768)
			return false;
		SetSize(320, 200, RECOILResolution.AppleII1x1);
		for (int c = 0; c < 256; c++) {
			int gb = content[0x7e00 + (c << 1)];
			int r = content[0x7e01 + (c << 1)] & 0xf;
			int g = gb >> 4;
			int b = gb & 0xf;
			int rgb = r << 16 | g << 8 | b;
			rgb |= rgb << 4;
			ContentPalette[c] = rgb;
		}
		for (int y = 0; y < 200; y++) {
			int linePalette = (content[0x7d00 + y] & 0xf) << 4;
			for (int x = 0; x < 320; x++) {
				int b = content[y * 160 + (x >> 1)];
				int c = (x & 1) == 0 ? b >> 4 : b & 0xf;
				Pixels[y * 320 + x] = ContentPalette[linePalette + c];
			}
		}
		return true;
	}

	// SAM Coupe formats.

	bool DecodeScs4(byte[] content, int contentLength)
	{
		if (contentLength != 24617 || content[24616] != 0xff)
			return false;

		int[16] palette;
		for (int i = 0; i < 16; i++) {
			int c = content[0x6000 + i];
			int rgb = 0;
			if ((c & 1) != 0)
				rgb |= 0x000049; // B1
			if ((c & 2) != 0)
				rgb |= 0x490000; // R1
			if ((c & 4) != 0)
				rgb |= 0x004900; // G1
			if ((c & 8) != 0)
				rgb |= 0x242424; // BRIGHT
			if ((c & 0x10) != 0)
				rgb |= 0x000092; // B2
			if ((c & 0x20) != 0)
				rgb |= 0x920000; // R2
			if ((c & 0x40) != 0)
				rgb |= 0x009200; // G2
			palette[i] = rgb;
		}

		SetSize(256, 192, RECOILResolution.SamCoupe1x1);
		for (int y = 0; y < 192; y++) {
			for (int x = 0; x < 256; x++) {
				int c = content[(y << 7) + (x >> 1)];
				if ((x & 1) == 0)
					c >>= 4;
				Pixels[(y << 8) + x] = palette[c & 0xf];
			}
		}
		return true;
	}

	// Commodore 16 formats.

	void SetC16Palette()
	{
		byte[] pal = BinaryResource("c16.pal");
		for (int i = 0; i < 256; i++)
			ContentPalette[i] = pal[i * 3] << 16 | pal[i * 3 + 1] << 8 | pal[i * 3 + 2];
	}

	bool DecodeP4i(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 10050:
			if (IsStringAt(content, 0x3fc, "MULT")) {
				SetSize(320, 200, RECOILResolution.C162x1);
				SetC16Palette();
				for (int y = 0; y < 200; y++) {
					for (int x = 0; x < 320; x++) {
						int offset = (y & ~7) * 40 + (x & ~7) + (y & 7);
						int c = content[0x802 + offset] >> (~x & 6) & 3;
						switch (c) {
						case 0:
							c = content[0x401];
							break;
						case 1:
							offset >>= 3;
							c = (content[0x402 + offset] & 0xf0) | (content[2 + offset] & 7);
							break;
						case 2:
							offset >>= 3;
							c = (content[0x402 + offset] & 0xf) << 4 | content[2 + offset] >> 4;
							break;
						default:
							c = content[0x400];
							break;
						}
						Pixels[y * 320 + x] = ContentPalette[c];
					}
				}
			}
			else {
				SetSize(320, 200, RECOILResolution.C161x1);
				SetC16Palette();
				for (int y = 0; y < 200; y++) {
					for (int x = 0; x < 320; x++) {
						int offset = (y & ~7) * 40 + (x & ~7) + (y & 7);
						int c = content[0x802 + offset] >> (~x & 7) & 1;
						offset >>= 3;
						if (c == 0)
							c = (content[0x402 + offset] & 0xf) << 4 | content[2 + offset] >> 4;
						else
							c = (content[0x402 + offset] & 0xf0) | (content[2 + offset] & 7);
						Pixels[y * 320 + x] = ContentPalette[c];
					}
				}
			}
			break;
		case 2050:
			SetSize(256, 64, RECOILResolution.C162x1);
			SetC16Palette();
			for (int y = 0; y < 64; y++) {
				for (int x = 0; x < 256; x++) {
					const byte[4] logoColors = { 0, 0x13, 0x15, 0x17 };
					int c = logoColors[content[2 + ((x & ~7) << 3) + y] >> (~x & 6) & 3];
					Pixels[(y << 8) + x] = ContentPalette[c];
				}
			}
			break;
		default:
			return false;
		}
		return true;
	}

	// Commodore 64 formats.

	bool Decode64c(byte[] content, int contentLength)
	{
		if (contentLength < 10 || contentLength > 2050 || content[0] != 0)
			return false;
		SetSize(256, contentLength + 253 >> 8 << 3, RECOILResolution.C641x1);
		DecodeBlackAndWhiteFont(content, 2, contentLength);
		return true;
	}

	const int[16] C64Palette = {
		0x000000,
		0xffffff,
		0x68372b,
		0x70a4b2,
		0x6f3d86,
		0x588d43,
		0x352879,
		0xb8c76f,
		0x6f4f25,
		0x433900,
		0x9a6759,
		0x444444,
		0x6c6c6c,
		0x9ad284,
		0x6c5eb5,
		0x959595
	};

	bool Decode4bt(byte[] content, int contentLength)
	{
		if (contentLength < 5 || !IsStringAt(content, 0, "GOD0") || content[contentLength - 1] != 0xad)
			return false;
		byte[32000] unpacked;
		GoDotStream() rle;
		rle.Content = content;
		rle.ContentOffset = 4;
		rle.ContentLength = contentLength - 1;
		if (!rle.Unpack(unpacked, 0, 1, 32000))
			return false;
		SetSize(320, 200, RECOILResolution.C641x1);
		for (int y = 0; y < 200; y++) {
			for (int x = 0; x < 320; x++) {
				const byte[16] byBrightness = { 0, 6, 9, 11, 2, 4, 8, 12, 14, 10, 5, 15, 3, 7, 13, 1 };
				int c = unpacked[(y & ~7) * 160 + ((x & ~7) + (y & 7) << 2) + (x >> 1 & 3)];
				Pixels[y * 320 + x] = C64Palette[byBrightness[(x & 1) == 0 ? c >> 4 : c & 0xf]];
			}
		}
		return true;
	}

	const int FliBugCharacters = 3;

	void DecodeC64HiresFrame(byte[] content, int bitmapOffset, int videoMatrixOffset, int pixelsOffset)
	{
		bool afli = Width == 320 - FliBugCharacters * 8;
		for (int y = 0; y < Height; y++) {
			for (int x = 0; x < Width; x++) {
				int offset = (y & ~7) * 40 + (x & ~7) + (y & 7);
				int c = content[bitmapOffset + offset] >> (~x & 7) & 1;
				int v;
				if (videoMatrixOffset >= 0) {
					offset >>= 3;
					if (afli)
						offset += (y & 7) << 10;
					v = content[videoMatrixOffset + offset];
				}
				else
					v = -videoMatrixOffset;
				c = c == 0 ? v & 0xf : v >> 4;
				Pixels[pixelsOffset + y * Width + x] = C64Palette[c];
			}
		}
	}

#if FUNPAINT_BORDER
	const int FunPaintBackground = -0x3f5a;
#else
	const int FunPaintBackground = 0;
#endif
	const int GunPaintBackground = -0x3f51;

	void DecodeC64MulticolorFrame(byte[] content, int bitmapOffset, int videoMatrixOffset, int colorOffset, int background, int pixelsOffset)
	{
		bool fli = Width == 320 - FliBugCharacters * 8;
		bool bottomBfli = pixelsOffset != 0 && Height == 400;
		for (int y = 0; y < 200; y++) {
			int lineBackground;
			if (background >= 0)
				lineBackground = background;
#if FUNPAINT_BORDER
			else if (background == FunPaintBackground && y >= 100)
				lineBackground = content[0x833a - 100 + y];
#endif
			else if (background == GunPaintBackground && y >= 177)
				lineBackground = content[y < 197 ? 0x47ea - 177 + y : 0x47fd];
			else
				lineBackground = content[y - background];
			for (int x = 0; x < Width; x++) {
				int c = lineBackground;
				int i = x + LeftSkip;
				if (i >= 0) {
					int offset = (y & ~7) * 40 + (i & ~7) + (y & 7);
					if (bottomBfli)
						offset = offset - (24 - FliBugCharacters) * 8 & 0x1fff;
					switch (content[bitmapOffset + offset] >> (~i & 6) & 3) {
					case 1:
						offset >>= 3;
						if (fli)
							offset += (y & 7) << 10;
						c = content[videoMatrixOffset + offset] >> 4;
						break;
					case 2:
						offset >>= 3;
						if (fli)
							offset += (y & 7) << 10;
						c = content[videoMatrixOffset + offset];
						break;
					case 3:
						c = colorOffset < 0 ? content[-colorOffset] : content[colorOffset + (offset >> 3)];
						break;
					default:
						break;
					}
				}
				Pixels[pixelsOffset + y * Width + x] = C64Palette[c & 0xf];
			}
		}
	}

	bool DecodeC64Multicolor(int width, byte[] content, int bitmapOffset, int videoMatrixOffset, int colorOffset, int background)
	{
		SetSize(width, 200, RECOILResolution.C642x1);
		DecodeC64MulticolorFrame(content, bitmapOffset, videoMatrixOffset, colorOffset, background, 0);
		return true;
	}

	bool DecodeOcp(byte[] content, int contentLength)
		return contentLength == 10018 && DecodeC64Multicolor(320, content, 2, 0x1f42, 0x233a, content[0x232b]);

	bool DecodeHir(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 8002: // FGS, GIH, HBM, HIR
		case 8194: // GCD, MON
			break;
		default:
			return false;
		}
		SetSize(320, 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2, -0x10, 0);
		return true;
	}

	bool DecodeIhe(byte[] content, int contentLength)
	{
		if (contentLength != 16194)
			return false;
		SetSize(320, 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2, -0x0c, 0);
		DecodeC64HiresFrame(content, 0x2002, -0x0c, 320 * 200);
		return ApplyBlend();
	}

	bool DecodeIph(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 9002: // IPH
		case 9003: // HPC
		case 9009: // AAS, ART
			break;
		default:
			return false;
		}
		SetSize(320, 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2, 0x1f42, 0);
		return true;
	}

	bool DecodeHed(byte[] content, int contentLength)
	{
		if (contentLength != 9218)
			return false;
		SetSize(320, 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2, 0x2002, 0);
		return true;
	}

	bool DecodeDd(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 9026:
		case 9218:
		case 9346:
			break;
		default:
			return false;
		}
		SetSize(320, 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 0x402, 2, 0);
		return true;
	}

	bool DecodeJj(byte[] content, int contentLength)
	{
		if (contentLength < 2)
			return false;
		byte[9026] unpacked;
		C64KoalaStream() rle;
		rle.Content = content;
		rle.ContentOffset = 2;
		rle.ContentLength = contentLength;
		return rle.UnpackC64(unpacked, 9026)
			&& DecodeDd(unpacked, 9026);
	}

	bool DecodeHfc(byte[] content, int contentLength)
	{
		if (contentLength != 16386)
			return false;
		SetSize(320 - FliBugCharacters * 8, 112, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2 + FliBugCharacters * 8, 0x2002 + FliBugCharacters, 0);
		return true;
	}

	bool DecodeAfl(byte[] content, int contentLength)
	{
		if (contentLength != 16385)
			return false;
		SetSize(320 - FliBugCharacters * 8, 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 0x2002 + FliBugCharacters * 8, 2 + FliBugCharacters, 0);
		return true;
	}

	bool DecodePmg(byte[] content, int contentLength)
	{
		if (contentLength != 9332)
			return false;
		return DecodeC64Multicolor(320, content, 0x74, 0x2074, -0x1fb7, content[0x1fb4]);
	}

	bool DecodeKoa(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 10003: // GIG, KOA
		case 10006: // RPM
			break;
		default:
			return false;
		}
		return DecodeC64Multicolor(320, content, 2, 0x1f42, 0x232a, content[0x2712]);
	}

	bool DecodeGg(byte[] content, int contentLength)
	{
		if (contentLength < 2)
			return false;
		byte[10003] unpacked;
		C64KoalaStream() rle;
		rle.Content = content;
		rle.ContentOffset = 2;
		rle.ContentLength = contentLength;
		return rle.UnpackC64(unpacked, 10003)
			&& DecodeKoa(unpacked, 10003);
	}

	bool DecodeDol(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 10241: // VIC
		case 10242: // DOL
		case 10050: // VID
			break;
		default:
			return false;
		}
		return DecodeC64Multicolor(320, content, 0x802, 0x402, 2, content[0x7ea]);
	}

	bool DecodeAmi(byte[] content, int contentLength)
	{
		if (contentLength < 2)
			return false;
		byte[10259] unpacked;
		DrpStream() rle;
		rle.Content = content;
		rle.ContentOffset = 2;
		rle.ContentLength = contentLength;
		rle.Escape = 0xc2;
		return rle.UnpackC64(unpacked, 10259)
			&& DecodeKoa(unpacked, 10003);
	}

	bool DecodeHlf(byte[] content, int contentLength)
	{
		if (contentLength != 24578)
			return false;
		SetSize(320 , 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2, 0x2402, 0);
		DecodeC64HiresFrame(content, 0x4002, 0x2802, 320 * 200);
		return ApplyBlend();
	}

	bool DecodeMci(byte[] content, int contentLength)
	{
		if (contentLength != 19434)
			return false;
		SetSize(320, 200, RECOILResolution.C641x1);
		DecodeC64MulticolorFrame(content, 0x402, 2, 0x4802, content[0x3ea], 0);
		LeftSkip = -1;
		DecodeC64MulticolorFrame(content, 0x2402, 0x4402, 0x4802, content[0x3ea], 320 * 200);
		return ApplyBlend();
	}

	bool DecodeDrz(byte[] content, int contentLength)
	{
		byte[10051] unpacked;
		content = DrpStream.UnpackFile(content, contentLength, "DRAZPAINT 1.4", unpacked, 10051);
		return content != null
			&& DecodeC64Multicolor(320, content, 0x802, 0x402, 2, content[0x2742]);
	}

	bool DecodeDrl(byte[] content, int contentLength)
	{
		byte[18242] unpacked;
		content = DrpStream.UnpackFile(content, contentLength, "DRAZLACE! 1.0", unpacked, 18242);
		if (content == null)
			return false;
		int shift = content[0x2744];
		if (shift > 1)
			return false;
		SetSize(320, 200, shift == 0 ? RECOILResolution.C642x1 : RECOILResolution.C641x1);
		DecodeC64MulticolorFrame(content, 0x802, 0x402, 2, content[0x2742], 0);
		LeftSkip = -shift;
		DecodeC64MulticolorFrame(content, 0x2802, 0x402, 2, content[0x2742], 320 * 200);
		return ApplyBlend();
	}

	void DecodeMleFrame(byte[] content, int contentOffset, int pixelsOffset)
	{
		for (int y = 0; y < 56; y++) {
			for (int x = 0; x < 320; x++) {
				int c = 0;
				int i = x + LeftSkip;
				if (i >= 0) {
					int ch = (y >> 3) * 40 + (i >> 3);
					if (ch < 256) {
						const byte[4] colors = { 0, 9, 8, 5 };
						c = colors[content[contentOffset + (ch << 3) + (y & 7)] >> (~i & 6) & 3];
					}
				}
				Pixels[pixelsOffset + y * Width + x] = C64Palette[c];
			}
		}
	}

	bool DecodeMle(byte[] content, int contentLength)
	{
		if (contentLength != 4098)
			return false;
		SetSize(320, 56, RECOILResolution.C641x1);
		DecodeMleFrame(content, 2050, 0);
		LeftSkip = -1;
		DecodeMleFrame(content, 2, 320 * 56);
		return ApplyBlend();
	}

	bool DecodeHimUnpacked(byte[] content)
	{
		SetSize(320 - FliBugCharacters * 8, 192, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2 + 40 * 8 + FliBugCharacters * 8, 0x2002 + 40 + FliBugCharacters, 0);
		return true;
	}

	bool DecodeHim(byte[] content, int contentLength)
	{
		if (contentLength < 18 || content[0] != 0 || content[1] != 0x40)
			return false;
		if (content[3] == 0xff)
			return contentLength == 16385 && DecodeHimUnpacked(content);

		if (content[2] + (content[3] << 8) != 0x4000 - 3 + contentLength
		 || content[4] != 0xf2 || content[5] != 0x7f)
			return false;
		byte[16372] unpacked;
		HimStream() rle;
		rle.Content = content;
		rle.ContentOffset = contentLength - 1;
		for (int unpackedOffset = 0x3ff3; unpackedOffset >= 2 + 320; unpackedOffset--) {
			int b = rle.ReadRle();
			if (b < 0)
				return false;
			unpacked[unpackedOffset] = b;
		}
		return DecodeHimUnpacked(unpacked);
	}

	bool DecodeEci(byte[] content, int contentLength)
	{
		if (contentLength != 32770)
			return false;
		SetSize(320 - FliBugCharacters * 8, 200, RECOILResolution.C641x1);
		DecodeC64HiresFrame(content, 2 + FliBugCharacters * 8, 0x2002 + FliBugCharacters, 0);
		DecodeC64HiresFrame(content, 0x4002 + FliBugCharacters * 8, 0x6002 + FliBugCharacters, 296 * 200);
		return ApplyBlend();
	}

	bool DecodeEcp(byte[] content, int contentLength)
	{
		if (contentLength < 6)
			return false;
		byte[32770] unpacked;
		DrpStream() rle;
		rle.Content = content;
		rle.ContentOffset = 3;
		rle.ContentLength = contentLength;
		rle.Escape = content[2];
		return rle.Unpack(unpacked, 2, 1, 32768)
			&& DecodeEci(unpacked, 32770);
	}

	bool DecodeFli(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 17218: // FD2
		case 17409: // FLI
		case 17410: // VIC
			break;
		default:
			return false;
		}
		DecodeC64Multicolor(320 - FliBugCharacters * 8, content, 0x2402 + FliBugCharacters * 8, 0x402 + FliBugCharacters, 2 + FliBugCharacters, 0);
		return true;
	}

	bool DecodeBml(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 17474: // BML
		case 17665: // VIC
		case 17666: // VIC
			break;
		default:
			return false;
		}
		DecodeC64Multicolor(320 - FliBugCharacters * 8, content, 0x2502 + FliBugCharacters * 8, 0x502 + FliBugCharacters, 0x102 + FliBugCharacters, -2);
		return true;
	}

	bool DecodeFfli(byte[] content, int contentLength)
	{
		if (contentLength != 26115 || content[2] != 'f')
			return false;
		DecodeC64Multicolor(320 - FliBugCharacters * 8, content, 0x2503 + FliBugCharacters * 8, 0x503 + FliBugCharacters, 0x103 + FliBugCharacters, -3);
		DecodeC64MulticolorFrame(content, 0x2503 + FliBugCharacters * 8, 0x4503 + FliBugCharacters, 0x103 + FliBugCharacters, -0x6503, 296 * 200);
		return ApplyBlend();
	}

	bool DecodeGun(byte[] content, int contentLength)
	{
		if (contentLength != 33602 && contentLength != 33603)
			return false;
		//if (!IsStringAt(content, 0x3ea, "GUNPAINT (JZ)   "))
		//	return false;
		SetSize(320 - FliBugCharacters * 8, 200, RECOILResolution.C641x1);
		DecodeC64MulticolorFrame(content, 0x2002 + FliBugCharacters * 8, 2 + FliBugCharacters, 0x4002 + FliBugCharacters, GunPaintBackground, 0);
		LeftSkip = -1;
		DecodeC64MulticolorFrame(content, 0x6402 + FliBugCharacters * 8, 0x4402 + FliBugCharacters, 0x4002 + FliBugCharacters, GunPaintBackground, 296 * 200);
		return ApplyBlend();
	}

	bool DecodeFunUnpacked(byte[] content)
	{
		SetSize(320 - FliBugCharacters * 8, 200, RECOILResolution.C641x1);
		DecodeC64MulticolorFrame(content, 0x2012 + FliBugCharacters * 8, 0x12 + FliBugCharacters, 0x4012 + FliBugCharacters, FunPaintBackground, 0);
		LeftSkip = -1;
		DecodeC64MulticolorFrame(content, 0x63fa + FliBugCharacters * 8, 0x43fa + FliBugCharacters, 0x4012 + FliBugCharacters, FunPaintBackground, 296 * 200);
		return ApplyBlend();
	}

	bool DecodeFun(byte[] content, int contentLength)
	{
		if (contentLength < 18 || !IsStringAt(content, 2, "FUNPAINT (MT) "))
			return false;
		if (content[16] != 0) {
			byte[33694] unpacked;
			DrpStream() rle;
			rle.Content = content;
			rle.ContentOffset = 18;
			rle.ContentLength = contentLength;
			rle.Escape = content[17];
			return rle.Unpack(unpacked, 18, 1, 33694 - 18)
				&& rle.ReadRle() < 0
				&& DecodeFunUnpacked(unpacked);
		}
		return contentLength == 33694 && DecodeFunUnpacked(content);
	}

	bool DecodeBfli(byte[] content, int contentLength)
	{
		if (contentLength != 33795 || content[2] != 'b')
			return false;
		SetSize(320 - FliBugCharacters * 8, 400, RECOILResolution.C642x1);
		DecodeC64MulticolorFrame(content, 0x2403 + FliBugCharacters * 8, 0x403 + FliBugCharacters, 3 + FliBugCharacters, 0, 0);
		DecodeC64MulticolorFrame(content, 0x6403, 0x4403, 3, 0, 296 * 200);
		return true;
	}

	bool DecodeLp3(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 4098:
		case 4174:
			break;
		default:
			return false;
		}
		if (content[0] != 0 || content[1] != 0x18)
			return false;
		SetSize(320, 400, RECOILResolution.C642x1);
		byte[4] colors;
		if (contentLength == 4174) {
			colors[0] = content[2 + 0x7fb] & 0xf;
			colors[1] = content[2 + 0x7fd] & 0xf;
			colors[2] = content[2 + 0x7fe] & 0xf;
			colors[3] = content[2 + 0x7fc] & 7;
		}
		else {
			colors[0] = 0;
			colors[1] = 0xa;
			colors[2] = 2;
			colors[3] = 1;
		}
		for (int y = 0; y < 400; y++) {
			for (int x = 0; x < 320; x++) {
				int c = content[2 + (y >> 3) * 40 + (x >> 3)];
				c = content[0x802 + (c << 3) + (y & 7)] >> (~x & 6) & 3;
				Pixels[y * 320 + x] = C64Palette[colors[c]];
			}
		}
		return true;
	}

	bool DecodeVic(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 9002:
		case 9003:
		case 9009:
			return DecodeIph(content, contentLength);
		case 10018:
			return DecodeOcp(content, contentLength);
		case 10241:
		case 10242:
			return DecodeDol(content, contentLength);
		case 17218:
		case 17409:
		case 17410:
			return DecodeFli(content, contentLength);
		case 17474:
		case 17665:
		case 17666:
			return DecodeBml(content, contentLength);
		case 18242:
			return DecodeDrl(content, contentLength);
		case 33602:
		case 33603:
			return DecodeGun(content, contentLength);
		case 33694:
			return DecodeFun(content, contentLength);
		default:
			return false;
		}
	}

	// Atari ST formats.

	bool DecodeDoo(byte[] content, int contentLength)
	{
		return contentLength == 32000 && DecodeBlackAndWhite(content, 0, 640, 400, 0xffffff, RECOILResolution.St1x1);
	}

	bool DecodePac(byte[] content, int contentLength)
	{
		if (contentLength < 8
		 || content[0] != 'p' || content[1] != 'M' || content[2] != '8')
			return false;
		int unpackedStride;
		switch (content[3]) {
		case '5':
			unpackedStride = 1;
			break;
		case '6':
			unpackedStride = 80;
			break;
		default:
			return false;
		}
		PacStream() rle;
		rle.Content = content;
		rle.ContentOffset = 7;
		rle.ContentLength = contentLength;
		byte[32000] unpacked;
		if (!rle.Unpack(unpacked, 0, unpackedStride, 32000))
			return false;
		return DecodeDoo(unpacked, 32000);
	}

	static bool IsStePalette(byte[] content, int contentOffset, int colors)
	{
		while (--colors >= 0) {
			if ((content[contentOffset] & 8) != 0 || (content[contentOffset + 1] & 0x88) != 0)
				return true;
			contentOffset += 2;
		}
		return false;
	}

	int GetStColor(byte[] content, int contentOffset)
	{
		int r = content[contentOffset];
		int gb = content[contentOffset + 1];
		int rgb;
		switch (Resolution) {
		case RECOILResolution.Ste1x1:
		case RECOILResolution.Ste1x2:
			// STE 4096 colors
			// xxxxrRRR gGGGbBBB -> 0000RRRr0000GGGg0000BBBb
			rgb = (r & 7) << 17 | (r & 8) << 13
				| (gb & 0x70) << 5 | (gb & 0x87) << 1 | (gb & 8) >> 3;
			return rgb << 4 | rgb;
		default:
			// ST 512 colors
			// xxxx0RRR 0GGG0BBB -> 00000RRR00000GGG00000BBB
			rgb = (r & 7) << 16
				| (gb & 0x70) << 4
				| (gb & 7);
			return rgb << 5 | rgb << 2 | (rgb >> 1 & 0x030303);
		}
	}

	void SetStPalette(byte[] content, int contentOffset, int colors)
	{
		for (int c = 0; c < colors; c++)
			ContentPalette[c] = GetStColor(content, contentOffset + c * 2);
	}

	static int GetStColor1000(byte[] content, int contentOffset)
	{
		int rgb = 0;
		for (int i = 0; i < 3; i++) {
			int c = content[contentOffset + i * 2] << 8 | content[contentOffset + i * 2 + 1];
			if (c > 1000)
				return -1;
			rgb = rgb << 8 | c * 255 / 1000;
		}
		return rgb;
	}

	bool DecodeStLow(byte[] bitmap, int bitmapOffset, byte[] palette, int paletteOffset, int width, int height)
	{
		SetSize(width, height, IsStePalette(palette, paletteOffset, 16) ? RECOILResolution.Ste1x1 : RECOILResolution.St1x1);
		SetStPalette(palette, paletteOffset, 16);
		DecodeBitplanes(bitmap, bitmapOffset, 4, 0, width * height);
		return true;
	}

	void DecodeStMedium(byte[] bitmap, int bitmapOffset, byte[] palette, int paletteOffset, int width, int height, int blend)
	{
		SetSize(width, height << 1, IsStePalette(palette, paletteOffset, 4) ? RECOILResolution.Ste1x2 : RECOILResolution.St1x2);
		SetStPalette(palette, paletteOffset, 4);
		height <<= blend;
		for (int y = 0; y < height; y++) {
			int lineOffset = y * width;
			for (int x = 0; x < width; x++) {
				int offset = bitmapOffset + (lineOffset >> 2) + (x >> 2 & ~3) + (x >> 3 & 1);
				int bit = ~x & 7;
				int c = bitmap[offset] >> bit & 1 | (bitmap[offset + 2] >> bit & 1) << 1;
				int pixelsOffset = (lineOffset << 1) + x;
				Pixels[pixelsOffset + width] = Pixels[pixelsOffset] = ContentPalette[c];
			}
		}
	}

	bool DecodeSt(byte[] bitmap, int bitmapOffset, byte[] palette, int paletteOffset, int mode)
	{
		switch (mode) {
		case 0:
			return DecodeStLow(bitmap, bitmapOffset, palette, paletteOffset, 320, 200);
		case 1:
			DecodeStMedium(bitmap, bitmapOffset, palette, paletteOffset, 640, 200, 0);
			return true;
		case 2:
			return DecodeBlackAndWhite(bitmap, bitmapOffset, 640, 400, 0xffffff, RECOILResolution.St1x1);
		default:
			return false;
		}
	}

	bool DecodeStPi(byte[] content, int contentLength)
	{
		if (contentLength < 32034 || content[0] != 0)
			return false;
		switch (contentLength) {
		case 32034: // DEGAS
		case 32066: // DEGAS Elite
		case 32128: // TROUBLE.PI1
			return DecodeSt(content, 34, content, 2, content[1]);
		case 44834: // 280 lines overscan
			if (content[1] != 0)
				return false;
			return DecodeStLow(content, 34, content, 2, 320, 280);
		case 116514: // 416x560 FORESTV.PI1
			if (content[1] != 0)
				return false;
			return DecodeStLow(content, 34, content, 2, 416, 560);
		default:
			return false;
		}
	}

	bool DecodePc(byte[] content, int contentLength)
	{
		if (contentLength < 68 || content[0] != 0x80 || content[1] > 2)
			return false;
		int bitplanes = 4 >> content[1];
		PackBitsStream() rle;
		rle.Content = content;
		rle.ContentOffset = 34;
		rle.ContentLength = contentLength;
		byte[32000] unpacked;
		for (int y = 0; y < 200; y++) {
			for (int bitplane = 0; bitplane < bitplanes; bitplane++) {
				for (int x = 0; x < 40 << content[1]; x++) {
					int b = rle.ReadRle();
					if (b < 0)
						return false;
					unpacked[y * 160 + (x & ~1) * bitplanes + (bitplane << 1) + (x & 1)] = b;
				}
			}
		}
		return DecodeSt(unpacked, 0, content, 2, content[1]);
	}

	bool DecodeNeo(byte[] content, int contentLength)
	{
		if (contentLength != 32128 || content[0] != 0 || content[1] != 0 || content[2] != 0)
			return false;
		return DecodeSt(content, 128, content, 4, content[3]);
	}

	bool DecodeArtDirector(byte[] content, int contentLength)
	{
		if (contentLength != 32512)
			return false;
		return DecodeStLow(content, 0, content, 32000, 320, 200);
	}

	bool DecodeGfb(byte[] content, int contentLength)
	{
		if (contentLength < 20 || !IsStringAt(content, 0, "GF25"))
			return false;
		int bitplanes;
		switch (Get32BigEndian(content, 4)) {
		case   2: bitplanes = 1; break;
		case   4: bitplanes = 2; break;
		case  16: bitplanes = 4; break;
		case 256: bitplanes = 8; break;
		default: return false;
		}
		int width = Get32BigEndian(content, 8);
		int height = Get32BigEndian(content, 12);
		int bitmapLength = Get32BigEndian(content, 16);
		if (1556 + bitmapLength != contentLength || bitmapLength != (width >> 3) * bitplanes * height)
			return false;

		for (int i = 0; i < 1 << bitplanes; i++) {
			int j;
			switch (i) {
			case   1: j = (1 << bitplanes) - 1; break;
			case   2: j = 1; break;
			case   3: j = 2; break;
			case   5: j = 6; break;
			case   6: j = 3; break;
			case   7: j = 5; break;
			case   8: j = 7; break;
			case   9: j = 8; break;
			case  10: j = 9; break;
			case  11: j = 10; break;
			case  13: j = 14; break;
			case  14: j = 11; break;
			case  15: j = 13; break;
			case 255: j = 15; break;
			default:  j = i; break;
			}
			int rgb = GetStColor1000(content, 20 + bitmapLength + i * 6);
			if (rgb < 0)
				return false;
			ContentPalette[j] = rgb;
		}

		if (!SetSizeStOrFalcon(width, height, bitplanes))
			return false;
		DecodeBitplanes(content, 20, bitplanes, 0, width * height);
		return true;
	}

	bool DecodeCa(byte[] content, int contentLength)
	{
		if (contentLength < 8
		 || content[0] != 'C' || content[1] != 'A')
			return false;
		int contentOffset;
		switch (content[3]) {
		case 0:
			contentOffset = 4 + 32;
			break;
		case 1:
			contentOffset = 4 + 8;
			break;
		case 2:
			contentOffset = 4;
			break;
		default:
			return false;
		}
		switch (content[2]) {
		case 0:
			if (contentOffset + 32000 != contentLength)
				return false;
			return DecodeSt(content, contentOffset, content, 4, content[3]);
		case 1:
			byte[32000] unpacked;
			if (!CaStream.UnpackCa(content, contentOffset, contentLength, unpacked, 0))
				return false;
			return DecodeSt(unpacked, 0, content, 4, content[3]);
		default:
			return false;
		}
	}

	bool DecodeTny(byte[] content, int contentLength)
	{
		if (contentLength < 42)
			return false;
		int mode = content[0];
		int contentOffset;
		if (mode > 2) {
			if (mode > 5)
				return false;
			mode -= 3;
			contentOffset = 4;
		}
		else
			contentOffset = 0;

		int controlLength = content[contentOffset + 33] << 8 | content[contentOffset + 34];
		int valueLength = (content[contentOffset + 35] << 8 | content[contentOffset + 36]) << 1;
		if (contentOffset + 37 + controlLength + valueLength > contentLength)
			return false;
		TnyStream() rle;
		rle.Content = content;
		rle.ContentOffset = contentOffset + 37;
		rle.ValueOffset = rle.ContentLength = contentOffset + 37 + controlLength;
		rle.ValueLength = contentOffset + 37 + controlLength + valueLength;

		byte[32000] unpacked;
		for (int bitplane = 0; bitplane < 8; bitplane += 2) {
			for (int x = bitplane; x < 160; x += 8) {
				for (int unpackedOffset = x; unpackedOffset < 32000; unpackedOffset += 160) {
					int b = rle.ReadRle();
					if (b < 0)
						return false;
					unpacked[unpackedOffset] = b >> 8;
					unpacked[unpackedOffset + 1] = b.LowByte;
				}
			}
		}

		return DecodeSt(unpacked, 0, content, contentOffset + 1, mode);
	}

	bool DecodeCpt(byte[] content, int contentLength)
	{
		if (contentLength < 40 || content[32] != 0 || content[33] > 2)
			return false;
		int bitplanes = 4 >> content[33];
		byte[32000] unpacked;
		byte[16000] isFilled = 0;
		int contentOffset = 34;

		// RLE
		for (;;) {
			int nextContentOffset = contentOffset + 4 + bitplanes * 2;
			if (nextContentOffset > contentLength)
				return false;
			int repeatCount = content[contentOffset] << 8 | content[contentOffset + 1];
			if (repeatCount == 0xffff) {
				contentOffset = nextContentOffset;
				break;
			}
			int offset = (content[contentOffset + 2] << 8 | content[contentOffset + 3]) * bitplanes;
			do {
				if (offset >= 16000)
					return false;
				for (int bitplane = 0; bitplane < bitplanes; bitplane++) {
					unpacked[offset * 2] = content[contentOffset + 4 + bitplane * 2];
					unpacked[offset * 2 + 1] = content[contentOffset + 4 + bitplane * 2 + 1];
					isFilled[offset++] = 1;
				}
			} while (--repeatCount >= 0);
			contentOffset = nextContentOffset;
		}

		// fill in holes
		for (int offset = 0; offset < 16000; offset++) {
			if (isFilled[offset] == 0) {
				if (contentOffset + 1 >= contentLength)
					return false;
				unpacked[offset * 2] = content[contentOffset++];
				unpacked[offset * 2 + 1] = content[contentOffset++];
			}
		}

		return DecodeSt(unpacked, 0, content, 0, content[33]);
	}

	bool DecodeStImg(byte[] content, int contentLength)
	{
		// TODO: content[1] == 3
		if (contentLength < 17 || content[0] != 0 || content[1] != 1 || content[4] != 0)
			return false;
		int headerLength = (content[2] << 8 | content[3]) << 1;
		if (headerLength < 16 || headerLength >= contentLength)
			return false;
		int bitplanes = content[5];
		int width = (content[12] << 8 | content[13]) + 7 & ~7; // round up to 8 pixels; FIXME: these pixels are garbage
		int height = content[14] << 8 | content[15];

		switch (bitplanes) {
		case 1:
			ContentPalette[0] = 0xffffff;
			ContentPalette[1] = 0x000000;
			break;
		case 2:
		case 4:
		case 8:
			if (headerLength == 22 + (6 << bitplanes) && IsStringAt(content, 16, "XIMG") && content[20] == 0 && content[21] == 0) {
				for (int i = 0; i < 1 << bitplanes; i++) {
					int rgb = GetStColor1000(content, 22 + i * 6);
					if (rgb < 0)
						return false;
					ContentPalette[i] = rgb;
				}
			}
			else
				return false; // TODO: default palette?
			break;
		// TODO: 15, 16, 24 bitplanes
		default:
			return false;
		}

		if (!SetSizeStOrFalcon(width, height, bitplanes))
			return false;
		ImgStream() rle;
		rle.Content = content;
		rle.ContentOffset = headerLength;
		rle.ContentLength = contentLength;
		byte[MaxWidth / 8 * 8] unpacked;
		for (int y = 0; y < height; ) {
			int lineRepeatCount = rle.GetLineRepeatCount();
			for (int bitplane = 0; bitplane < bitplanes; bitplane++) {
				for (int x = 0; x < width >> 3; x++) {
					int b = rle.ReadRle();
					if (b < 0)
						return false;
					unpacked[(x & ~1) * bitplanes + (bitplane << 1) + (x & 1)] = b;
				}
			}
			do
				DecodeBitplanes(unpacked, 0, bitplanes, y * width, width);
			while (++y < height && --lineRepeatCount > 0);
		}
		return true;
	}

	bool DecodeDuo(byte[] content, int contentLength)
	{
		if (contentLength != 113600)
			return false;
		DecodeStLow(content, 32, content, 0, 416, 273);
		DecodeBitplanes(content, 32 + 208 * 273, 4, 416 * 273, 416 * 273);
		return ApplyBlend();
	}

	bool DecodeDu2(byte[] content, int contentLength)
	{
		if (contentLength != 113576 && contentLength != 113600)
			return false;
		DecodeStMedium(content, 8, content, 0, 832, 273, 1);
		return ApplyBlend();
	}

	bool UnpackLz4(byte[] content, int contentLength, byte[] unpacked, int unpackedLength)
	{
		if (contentLength < 4 + 3 + 4
		 || content[0] != 0x04 || content[1] != 0x22 || content[2] != 0x4d || content[3] != 0x18
		 || (content[4] & 0xc3) != 0x40)
			return false;
		Lz4Stream() stream;
		stream.Content = content;
		stream.ContentOffset = 6 + 1;
		if ((content[4] & 0x08) != 0)
			stream.ContentOffset += 8; // content size
		stream.Unpacked = unpacked;
		stream.UnpackedOffset = 0;
		stream.UnpackedLength = unpackedLength;

		for (;;) {
			if (stream.ContentOffset + 4 > contentLength)
				return false;
			int blockSize = Get32LittleEndian(content, stream.ContentOffset);
			stream.ContentOffset += 4;
			stream.ContentLength = contentLength;

			if (blockSize == 0)
				break;
			if ((blockSize >> 31) != 0) {
				if (!stream.Copy(blockSize & 0x7fffffff))
					return false;
				continue;
			}
			stream.ContentLength = stream.ContentOffset + blockSize;
			if (stream.ContentLength > contentLength)
				return false;

			for (;;) {
				int token = stream.ReadByte();
				if (token < 0)
					return false;

				// literals
				int count = stream.ReadCount(token >> 4);
				if (count < 0
				 || !stream.Copy(count))
					return false;

				if (stream.ContentOffset == stream.ContentLength)
					break;

				// LZ77
				if (stream.ContentOffset + 2 > stream.ContentLength)
					return false;
				int offset = stream.ReadByte();
				offset += stream.ReadByte() << 8;
				if (offset == 0 || offset > stream.UnpackedOffset)
					return false;
				count = stream.ReadCount(token & 0xf);
				if (count < 0
				 || stream.UnpackedOffset + count > unpackedLength)
					return false;
				count += 4;
				do {
					unpacked[stream.UnpackedOffset] = unpacked[stream.UnpackedOffset - offset];
					stream.UnpackedOffset++;
				} while (--count > 0);
			}

			if ((content[4] & 0x10) != 0)
				stream.ContentOffset += 4; // block checksum
		}

		if ((content[4] & 0x04) != 0)
			stream.ContentOffset += 4; // content checksum
		return stream.ContentOffset == contentLength && stream.UnpackedOffset == unpackedLength;
	}

	bool DecodePl4(byte[] content, int contentLength)
	{
		byte[64070] unpacked;
		const int secondPi1Offset = 32036;
		if (!UnpackLz4(content, contentLength, unpacked, unpacked.Length)
		 || unpacked[0] != 0 || unpacked[1] != 0
		 || unpacked[secondPi1Offset] != 0 || unpacked[secondPi1Offset + 1] != 0)
			return false;
		SetSize(320, 200, IsStePalette(unpacked, 2, 16) || IsStePalette(unpacked, secondPi1Offset + 2, 16) ? RECOILResolution.Ste1x1 : RECOILResolution.St1x1);
		SetStPalette(unpacked, 2, 16);
		DecodeBitplanes(unpacked, 34, 4, 0, 320 * 200);
		SetStPalette(unpacked, secondPi1Offset + 2, 16);
		DecodeBitplanes(unpacked, secondPi1Offset + 34, 4, 320 * 200, 320 * 200);
		return ApplyBlend();
	}

	bool DecodeSpu(byte[] content, int contentLength)
	{
		if (contentLength != 51104)
			return false;
		SetSize(320, 199, IsStePalette(content, 32000, 199 * 3 * 16) ? RECOILResolution.Ste1x1 : RECOILResolution.St1x1);
		int pixelsOffset = 0;
		for (int y = 0; y < 199; y++) {
			for (int x = 0; x < 320; x++) {
				int bitmapOffset = 160 + (pixelsOffset >> 1 & ~7) + (x >> 3 & 1);
				int bit = ~x & 7;
				int c = (content[bitmapOffset] >> bit & 1) << 1
					| (content[bitmapOffset + 2] >> bit & 1) << 2
					| (content[bitmapOffset + 4] >> bit & 1) << 3
					| (content[bitmapOffset + 6] >> bit & 1) << 4;
				// http://www.atari-forum.com/wiki/index.php?title=ST_Picture_Formats
				int x1 = c * 5 + 1 - (c & 2) * 3;
				if (x >= x1 + 160)
					c += 64;
				else if (x >= x1)
					c += 32;
				Pixels[pixelsOffset++] = GetStColor(content, 32000 + y * 96 + c);
			}
		}
		return true;
	}

	static bool UnpackSpc(RleStream rle, byte[] unpacked)
	{
		for (int bitplane = 0; bitplane < 8; bitplane += 2) {
			for (int unpackedOffset = 160 + bitplane; unpackedOffset < 32000; unpackedOffset += 8) {
				for (int x = 0; x < 2; x++) {
					int b = rle.ReadRle();
					if (b < 0)
						return false;
					unpacked[unpackedOffset + x] = b;
				}
			}
		}
		return true;
	}

	bool DecodeStSpc(byte[] content, int contentLength)
	{
		if (contentLength < 12
		 || content[0] != 'S' || content[1] != 'P')
			return false;
		byte[51104] unpacked;

		// bitmap
		SpcStream() rle;
		rle.Content = content;
		rle.ContentOffset = 12;
		rle.ContentLength = contentLength;
		if (!UnpackSpc(rle, unpacked))
			return false;

		// palettes
		int contentOffset = 12 + Get32BigEndian(content, 4);
		for (int unpackedOffset = 32000; unpackedOffset < 51104; ) {
			if (contentOffset + 1 >= contentLength)
				return false;
			int got = content[contentOffset] << 8 | content[contentOffset + 1];
			contentOffset += 2;
			for (int i = 0; i < 16; i++) {
				if ((got >> i & 1) == 0) {
					unpacked[unpackedOffset] = 0;
					unpacked[unpackedOffset + 1] = 0;
				}
				else {
					if (contentOffset + 1 >= contentLength)
						return false;
					unpacked[unpackedOffset] = content[contentOffset];
					unpacked[unpackedOffset + 1] = content[contentOffset + 1];
					contentOffset += 2;
				}
				unpackedOffset += 2;
			}
		}

		return DecodeSpu(unpacked, 51104);
	}

	bool DecodeSps(byte[] content, int contentLength)
	{
		if (contentLength < 13
		 || content[0] != 'S' || content[1] != 'P' || content[2] != 0 || content[3] != 0)
			return false;
		byte[51104] unpacked;

		// bitmap
		SpsStream() rle;
		rle.Content = content;
		rle.ContentOffset = 12;
		rle.ContentLength = contentLength;
		if ((content[contentLength - 1] & 1) == 0) {
			for (int bitplane = 0; bitplane < 8; bitplane += 2) {
				for (int x = 0; x < 40; x++) {
					for (int unpackedOffset = 160 + ((x & ~1) << 2) + bitplane + (x & 1); unpackedOffset < 32000; unpackedOffset += 160) {
						int b = rle.ReadRle();
						if (b < 0)
							return false;
						unpacked[unpackedOffset] = b;
					}
				}
			}
		}
		else {
			if (!UnpackSpc(rle, unpacked))
				return false;
		}

		// palettes
		BitStream() bitStream;
		bitStream.Content = content;
		bitStream.ContentOffset = 12 + Get32BigEndian(content, 4);
		bitStream.ContentLength = contentLength;
		for (int unpackedOffset = 32000; unpackedOffset < 51104; ) {
			int got = bitStream.ReadBits(14);
			if (got < 0)
				return false;
			got <<= 1;
			for (int i = 15; i >= 0; i--) {
				int rgb;
				if ((got >> i & 1) == 0)
					rgb = 0;
				else {
					rgb = bitStream.ReadBits(9);
					if (rgb < 0)
						return false;
				}
				// RRRGGGBBB -> 00000RRR 0GGG0BBB
				unpacked[unpackedOffset] = rgb >> 6;
				unpacked[unpackedOffset + 1] = (rgb & 0x3f) + (rgb & 0x38);
				unpackedOffset += 2;
			}
		}

		return DecodeSpu(unpacked, 51104);
	}

	void DecodePcsScreen(byte[] unpacked, int pixelsOffset)
	{
		for (int y = 0; y < 199; y++) {
			for (int x = 0; x < 320; x++) {
				int bitmapOffset = 40 + y * 40 + (x >> 3);
				int bit = ~x & 7;
				int c = (unpacked[bitmapOffset] >> bit & 1) << 1
					| (unpacked[8000 + bitmapOffset] >> bit & 1) << 2
					| (unpacked[16000 + bitmapOffset] >> bit & 1) << 3
					| (unpacked[24000 + bitmapOffset] >> bit & 1) << 4;
				// http://www.atari-forum.com/wiki/index.php?title=ST_Picture_Formats
				if (x >= c * 2) {
					if (c < 14 * 2) {
						if (x >= c * 2 + 76) {
							if (x >= 176 + c * 5 - (c & 2) * 3)
								c += 32;
							c += 32;
						}
					}
					else if (x >= c * 2 + 92)
						c += 32;
					c += 32;
				}
				Pixels[pixelsOffset++] = GetStColor(unpacked, 32000 + y * 96 + c);
			}
		}
	}

	bool DecodePcs(byte[] content, int contentLength)
	{
		if (contentLength < 18
		 || content[0] != 0x01 || content[1] != 0x40 || content[2] != 0x00 || content[3] != 0xc8)
			return false;

		PcsStream() rle;
		rle.Content = content;
		rle.ContentOffset = 6;
		rle.ContentLength = contentLength;
		byte[PcsStream.UnpackedLength] unpacked1;
		if (!rle.UnpackPcs(unpacked1))
			return false;
		// FIXME: I only check if first screen's colors are STE
		SetSize(320, 199, IsStePalette(unpacked1, 32000, 9616) ? RECOILResolution.Ste1x1 : RECOILResolution.St1x1);
		DecodePcsScreen(unpacked1, 0);
		if (content[4] == 0)
			return true; // single screen

		// alternating screens
		rle.RepeatCount = 0; // ignore possible overrun from first picture palettes
		byte[PcsStream.UnpackedLength] unpacked2;
		if (!rle.UnpackPcs(unpacked2))
			return false;
		if ((content[4] & 1) == 0) {
			for (int i = 0; i < 32000; i++)
				unpacked2[i] ^= unpacked1[i];
		}
		if ((content[4] & 2) == 0) {
			for (int i = 32000; i < PcsStream.UnpackedLength; i++)
				unpacked2[i] ^= unpacked1[i];
		}
		DecodePcsScreen(unpacked2, 320 * 199);
		return ApplyBlend();
	}

	void DecodeMppScreen(byte[] content, int paletteOffset, int paletteLength, int pixelsOffset)
	{
		int mode = content[3];
		int bitmapOffset = paletteOffset + paletteLength;
		int[16] palette = 0;
		MppPaletteStream() paletteStream;
		paletteStream.Content = content;
		paletteStream.ContentOffset = paletteOffset;
		paletteStream.ContentLength = bitmapOffset;

		for (int y = 0; y < Height; y++) {
			// For modes 0,1,2 palette[0] is black here.
			// No need to set it here, because it's black initially
			// and at the end of the previous line.
			// For mode 3 six colors remain from the previous line.
			for (int c = mode == 3 ? 6 : 1; c < 16; c++)
				palette[c] = paletteStream.Read();
			const byte[4] firstChangeX = { 33, 9, 4, 69 };
			int changeX = firstChangeX[mode];
			int changeColor = 0;
			for (int x = 0; x < Width; x++) {
				if (x == changeX) {
					// For modes 0,1,2 palette[0] is black in the last palette in line
					// and not stored in the file.
					// This is not the case for mode 3.
					const byte[4] rightBorderColor = { 0x20, 0x10, 0x20, 0x7f };
					palette[changeColor & 0xf] = changeColor == rightBorderColor[mode] ? 0 : paletteStream.Read();
					switch (mode) {
					case 0:
					case 3:
						switch (changeColor) {
						case 15:
							changeX += mode == 0 ? 88 : 112;
							break;
						case 31:
							changeX += 12;
							break;
						case 37:
							changeX += 100;
							break;
						default:
							changeX += 4;
							break;
						}
						break;
					case 1:
						changeX += (changeColor & 1) == 0 ? 4 : 16;
						break;
					case 2:
						changeX += 8;
						break;
					}
					changeColor++;
				}

				int offset = bitmapOffset + (x >> 1 & ~7) + (x >> 3 & 1);
				int bit = ~x & 7;
				int c = (content[offset] >> bit & 1)
					| (content[offset + 2] >> bit & 1) << 1
					| (content[offset + 4] >> bit & 1) << 2
					| (content[offset + 6] >> bit & 1) << 3;
				Pixels[pixelsOffset + x] = palette[c];
			}
			bitmapOffset += Width >> 1;
			pixelsOffset += Width;
		}
	}

	bool DecodeMpp(byte[] content, int contentLength)
	{
		if (contentLength < 12
		 || content[0] != 'M' || content[1] != 'P' || content[2] != 'P')
			return false;

		int mode = content[3];
		if (mode > 3)
			return false;
		int width = mode < 3 ? 320 : 416;
		int height = mode < 3 ? 199 : 273;
		const byte[4] modeColorsPerLine = { 52, 46, 54, 48 };
		int paletteLength = modeColorsPerLine[mode] * height;
		switch (content[4] & 3) {
		case 0:
			SetSize(width, height, RECOILResolution.St1x1);
			paletteLength *= 9;
			break;
		case 1:
			SetSize(width, height, RECOILResolution.Ste1x1);
			paletteLength *= 12;
			break;
		case 3:
			SetSize(width, height, RECOILResolution.Ste1x1);
			Frames = 2;
			paletteLength *= 15;
			break;
		default:
			return false;
		}
		paletteLength = paletteLength + 15 >> 4 << 1;
		int paletteOffset = 12 + Get32BigEndian(content, 8);
		int pixelsLength = width * height;
		if (contentLength != paletteOffset + (paletteLength + (pixelsLength >> 1) << (content[4] >> 2 & 1)))
			return false;

		// alternating screens
		DecodeMppScreen(content, paletteOffset, paletteLength, 0);
		if ((content[4] & 4) == 0)
			return true;
		DecodeMppScreen(content, paletteOffset + paletteLength + (pixelsLength >> 1), paletteLength, pixelsLength);
		return ApplyBlend();
	}

	bool DecodeStIcn(byte[] content, int contentLength)
	{
		const int maxWidth = 256;
		const int maxHeight = 256;
		IcnParser() parser;
		parser.Content = content;
		parser.ContentOffset = 0;
		parser.ContentLength = contentLength;
		int width = parser.ParseDefine("ICON_W");
		if (width <= 0 || width >= maxWidth)
			return false;
		int height = parser.ParseDefine("ICON_H");
		if (height <= 0 || height >= maxHeight)
			return false;
		int size = parser.ParseDefine("ICONSIZE");
		if (size != (width + 0xf >> 4) * height)
			return false;
		if (!parser.Expect("int")
		 || !parser.Expect("image[ICONSIZE]")
		 || !parser.Expect("=")
		 || !parser.Expect("{"))
			return false;
		byte[maxWidth / 8 * maxHeight] bitmap;
		for (int i = 0; ; ) {
			int value = parser.ParseHex();
			if (value < 0)
				return false;
			bitmap[i * 2] = value >> 8;
			bitmap[i * 2 + 1] = value.LowByte;
			if (++i >= size)
				break;
			if (parser.ContentOffset >= contentLength || content[parser.ContentOffset++] != ',')
				return false;
		}
		if (!parser.Expect("};"))
			return false;
		return DecodeBlackAndWhite(bitmap, 0, width + 0xf & ~0xf, height, 0xffffff, RECOILResolution.St1x1);
	}

	// Atari Falcon formats.

	void SetFalconPalette(byte[] content, int contentOffset)
	{
		for (int i = 0; i < 256; i++) {
			int offset = contentOffset + (i << 2);
			ContentPalette[i] = content[offset] << 16 | content[offset + 1] << 8 | content[offset + 3];
		}
	}

	void DecodeFalconPalette320(byte[] content, int bitplanesOffset, int paletteOffset, int height)
	{
		SetFalconPalette(content, paletteOffset);
		SetSize(320, height, RECOILResolution.Falcon1x1);
		DecodeBitplanes(content, bitplanesOffset, 8, 0, 320 * height);
	}

	bool DecodePi4(byte[] content, int contentLength)
	{
		int height;
		switch (contentLength) {
		case 65024:
			height = 200;
			break;
		case 77824:
			height = 240;
			break;
		default:
			return false;
		}
		DecodeFalconPalette320(content, 1024, 0, height);
		return true;
	}

	bool DecodeDgu(byte[] content, int contentLength)
	{
		if (contentLength != 65032
		 || content[0] != 'D' || content[1] != 'G' || content[2] != 'U' || content[3] != 1
		 || content[4] != 1 || content[5] != 64 || content[6] != 0 || content[7] != 200)
			return false;
		DecodeFalconPalette320(content, 1032, 8, 200);
		return true;
	}

	bool DecodeDgc(byte[] content, int contentLength)
	{
		if (contentLength < 1042
		 || content[0] != 'D' || content[1] != 'G' || content[2] != 'C' || content[3] > 3
		 || content[4] != 1 || content[5] != 64 || content[6] != 0 || content[7] != 200)
			return false;
		if (content[3] == 0) {
			if (contentLength != 65034)
				return false;
			DecodeFalconPalette320(content, 1034, 10, 200);
			return true;
		}

		byte[64000] unpacked;
		int contentOffset = 1038;
		int valueBytes = 1 << content[3] - 1; // 1, 2 or 4
		int repeatCount = 0;
		for (int bitplane = 0; bitplane < 16; bitplane += 2) {
			for (int unpackedOffset = bitplane; unpackedOffset < 64000; unpackedOffset += 16) {
				for (int x = 0; x < 2; x++) {
					if (repeatCount == 0) {
						int nextContentOffset = contentOffset + content[3] * 2;
						if (nextContentOffset > contentLength)
							return false;
						switch (content[3]) {
						case 1:
							repeatCount = content[contentOffset] + 1;
							break;
						case 2:
							repeatCount = (content[contentOffset] << 8) + content[contentOffset + 1] + 1 << 1;
							break;
						case 3:
							repeatCount = (content[contentOffset] << 8) + content[contentOffset + 1] + 1 << 2;
							break;
						}
						contentOffset = nextContentOffset;
					}
					repeatCount--;
					unpacked[unpackedOffset + x] = content[contentOffset - valueBytes + (~repeatCount & valueBytes - 1)];
				}
			}
		}

		SetFalconPalette(content, 10);
		SetSize(320, 200, RECOILResolution.Falcon1x1);
		DecodeBitplanes(unpacked, 0, 8, 0, 320 * 200);
		return true;
	}

	bool DecodeDel(byte[] content, int contentLength)
	{
		byte[96000] unpacked;
		if (!CaStream.UnpackDel(content, contentLength, unpacked, 2))
			return false;
		return DecodePi4(unpacked, 77824);
	}

	bool DecodeDph(byte[] content, int contentLength)
	{
		byte[] unpacked = new byte[320000];
		if (!CaStream.UnpackDel(content, contentLength, unpacked, 10)) {
			delete unpacked;
			return false;
		}
		SetFalconPalette(unpacked, 0);
		SetSize(640, 480, RECOILResolution.Falcon1x1);
		for (int y = 0; y < 240; y++) {
			// Combine four 320x240 quarters:
			// 1 2
			// 3 4
			DecodeBitplanes(unpacked, 1024 + y * 320, 8, y * 640, 320);
			DecodeBitplanes(unpacked, 1024 + 76800 + y * 320, 8, y * 640 + 320, 320);
			DecodeBitplanes(unpacked, 1024 + 2 * 76800 + y * 320, 8, (240 + y) * 640, 320);
			DecodeBitplanes(unpacked, 1024 + 3 * 76800 + y * 320, 8, (240 + y) * 640 + 320, 320);
		}
		delete unpacked;
		return true;
	}

	bool DecodeFalconTrueColor(byte[] content, int contentLength, int contentOffset, int width, int height)
	{
		int pixelsCount = width * height;
		if (contentOffset + pixelsCount * 2 != contentLength
		 || !SetSize(width, height, RECOILResolution.Falcon1x1))
			return false;
		for (int i = 0; i < pixelsCount; i++) {
			int rg = content[contentOffset + i * 2];
			int gb = content[contentOffset + i * 2 + 1];
			int r = rg & 0xf8 | rg >> 5;
			int g = (rg & 7) << 3 | gb >> 5;
			g = g << 2 | g >> 4;
			int b = gb & 0x1f;
			b = b << 3 | b >> 2;
			Pixels[i] = r << 16 | g << 8 | b;
		}
		return true;
	}

	bool DecodeFalconTrueColorVariable(byte[] content, int contentLength, int widthOffset, int dataOffset)
	{
		int width = content[widthOffset] << 8 | content[widthOffset + 1];
		int height = content[widthOffset + 2] << 8 | content[widthOffset + 3];
		return DecodeFalconTrueColor(content, contentLength, dataOffset, width, height);
	}

	bool DecodeFtc(byte[] content, int contentLength)
		return DecodeFalconTrueColor(content, contentLength, 0, 384, 240);

	bool DecodeGod(byte[] content, int contentLength)
		return contentLength > 6 && DecodeFalconTrueColorVariable(content, contentLength, 2, 6);

	bool DecodeTrp(byte[] content, int contentLength)
	{
		if (contentLength < 9 || !IsStringAt(content, 0, "TRUP"))
			return false;
		return DecodeFalconTrueColorVariable(content, contentLength, 4, 8);
	}

	bool DecodeTru(byte[] content, int contentLength)
	{
		if (contentLength < 256 || !IsStringAt(content, 0, "Indy"))
			return false;
		return DecodeFalconTrueColorVariable(content, contentLength, 4, 256);
	}

	// Amiga formats.

	bool DecodeInfo(byte[] content, int contentLength)
	{
		if (contentLength < 98 || content[0] != 0xe3 || content[1] != 0x10 || content[2] != 0 || content[3] != 1)
			return false;

		const int[4] os1Palette = { 0x55AAFF, 0xFFFFFF, 0x000000, 0xFF8800 };
		const int[8] os2Palette = { 0x959595, 0x000000, 0xFFFFFF, 0x3B67A2, 0x7B7B7B, 0xAFAFAF, 0xAA907C, 0xFFA997 };
		int[] palette;
		switch (Get32BigEndian(content, 0x2c)) {
		case 0:
			palette = os1Palette;
			break;
		case 1:
			palette = os2Palette;
			break;
		default:
			return false;
		}

		int contentOffset = Get32BigEndian(content, 0x42) == 0 ? 0x4e : 0x4e + 0x38; // skip DrawerData if present
		int width = content[contentOffset + 4] << 8 | content[contentOffset + 5];
		int height = content[contentOffset + 6] << 8 | content[contentOffset + 7];
		int bitplanes = content[contentOffset + 8] << 8 | content[contentOffset + 9];
		switch (bitplanes) {
		case 2:
			break;
		case 3:
			if (palette == os1Palette)
				return false;
			break;
		default:
			return false;
		}
		width = width + 15 & ~15; // round up to 16 pixels, FIXME
		contentOffset += 0x14;
		if (contentOffset + height * (width >> 3) > contentLength)
			return false;
		if (!SetSize(width, height, RECOILResolution.Amiga1x1))
			return false;

		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				int bit = ~x & 7;
				int offset = contentOffset + y * (width >> 3) + (x >> 3);
				int c = 0;
				for (int bitplane = bitplanes; --bitplane >= 0; )
					c = c << 1 | content[offset + bitplane * height * (width >> 3)] >> bit & 1;
				Pixels[y * width + x] = palette[c];
			}
		}
		return true;
	}

	void DecodeHam(byte[] unpacked, int bitplanes)
	{
		int holdBits = bitplanes > 6 ? 6 : 4;
		int i = 0;
		for (int y = 0; y < Height; y++) {
			int rgb = ContentPalette[0];
			for (int x = 0; x < Width; x++) {
				int offset = (i >> 3 & ~1) * bitplanes + (i >> 3 & 1);
				int bit = ~i & 7;
				int c = 0;
				for (int bitplane = bitplanes; --bitplane >= 0; )
					c = c << 1 | unpacked[offset + (bitplane << 1)] >> bit & 1;

				switch (c >> holdBits) {
				case 0:
					rgb = ContentPalette[c];
					break;
				case 1: // blue
					c = c << 8 - holdBits & 0xff;
					c |= c >> holdBits;
					rgb = (rgb & 0xffff00) | c;
					break;
				case 2: // red
					c = c << 8 - holdBits & 0xff;
					c |= c >> holdBits;
					rgb = (rgb & 0x00ffff) | c << 16;
					break;
				case 3: // green
					c = c << 8 - holdBits & 0xff;
					c |= c >> holdBits;
					rgb = (rgb & 0xff00ff) | c << 8;
					break;
				}
				Pixels[i++] = rgb;
			}
		}
	}

	bool DecodeIffUnpacked(byte[] unpacked, int bitplanes, int colors, int camg)
	{
		if (bitplanes <= 8) {
			if (colors == 0) {
				// grayscale
				colors = 1 << bitplanes;
				for (int c = 0; c < colors; c++)
					ContentPalette[c] = c * 255 / colors * 0x010101;
			}
			else if ((camg & 0x880) == 0x80 && bitplanes == 6 && colors < 64) {
				// Amiga Extra Half-Brite
				// atarist.c by Hans Wessels says bitplanes check is necessary.
				// I also check if we don't already have 64 colors,
				// because I found such EHB files.
				for (int c = 0; c < 32; c++)
					ContentPalette[32 + c] = ContentPalette[c] >> 1 & 0x7f7f7f;
			}
			if ((camg & 0x800) != 0) {
				// Amiga Hold-And-Modify
				DecodeHam(unpacked, bitplanes);
			}
			else {
				// palette
				DecodeBitplanes(unpacked, 0, bitplanes, 0, Width * Height);
			}
		}
		else {
			// 24-bit or 32-bit true color
			int pixelsCount = Width * Height;
			for (int i = 0; i < pixelsCount; i++) {
				int offset = (i >> 3 & ~1) * bitplanes + (i >> 3 & 1);
				int bit = ~i & 7;
				int c = 0;
				for (int bitplane = 24; --bitplane >= 0; )
					c = c << 1 | unpacked[offset + (bitplane << 1)] >> bit & 1;
				// 0xBBGGRR -> 0xRRGGBB
				Pixels[i] = (c & 0xff) << 16 | (c & 0xff00) | c >> 16;
			}
		}
		delete unpacked;
		return true;
	}

	bool DecodeIff(byte[] content, int contentLength)
	{
		if (contentLength < 56 || !IsStringAt(content, 0, "FORM"))
			return false;
		int contentOffset = 8;
		if (IsStringAt(content, 8, "DPSTDPAH") && Get32BigEndian(content, 16) == 24 && IsStringAt(content, 44, "FORM"))
			contentOffset = 52;
		IffType type;
		if (IsStringAt(content, contentOffset, "ILBM"))
			type = IffType.Ilbm;
		else if (IsStringAt(content, contentOffset, "PBM "))
			type = IffType.Pbm;
		else if (IsStringAt(content, contentOffset, "ACBM"))
			type = IffType.Acbm;
		else
			return false;
		contentOffset += 4;

		int width = 0;
		int height = 0;
		int bitplanes = 0;
		bool hasMask = false;
		int compression = 0;
		int colors = 0;
		int camg = 0;
		while (contentOffset + 7 < contentLength) {
			int chunkLength = Get32BigEndian(content, contentOffset + 4);
			if (contentOffset + 8 + chunkLength > contentLength)
				break;
			if (IsStringAt(content, contentOffset, "BMHD") && chunkLength >= 11) {
				width = content[contentOffset + 8] << 8 | content[contentOffset + 9];
				height = content[contentOffset + 10] << 8 | content[contentOffset + 11];
				bitplanes = content[contentOffset + 16];
				hasMask = content[contentOffset + 17] == 1;
				compression = content[contentOffset + 18];
				if (type == IffType.Pbm) {
					if (bitplanes != 8
					 || compression > 1)
						return false;
					// FIXME: round up to 4 pixels; we shouldn't show the extra pixels
					width = width + 3 & ~3;
				}
				else {
					if (bitplanes == 0 || (bitplanes > 8 && bitplanes != 24 && bitplanes != 32)
					 || compression > 2)
						return false;
					// FIXME: round up to 16 pixels; we shouldn't show the extra pixels
					width = width + 15 & ~15;
				}
				int pixelsCount = width * height;
				if (pixelsCount <= 0 || pixelsCount > MaxPixelsLength)
					return false;
			}
			else if (IsStringAt(content, contentOffset, "CMAP")) {
				colors = chunkLength / 3;
				if (colors > 256)
					return false;
				int c;
				for (c = 0; c < colors; c++) {
					int offset = contentOffset + 8 + c * 3;
					ContentPalette[c] = content[offset] << 16 | content[offset + 1] << 8 | content[offset + 2];
				}
				for ( ; c < 256; c++)
					ContentPalette[c] = 0;
			}
			else if (IsStringAt(content, contentOffset, "CAMG") && chunkLength >= 4)
				camg = Get32BigEndian(content, contentOffset + 8);
			else if (IsStringAt(content, contentOffset, "BODY")) {
				if (width == 0)
					return false;
				int bytesPerLine = (width >> 3) * bitplanes;

				byte[] unpacked;
				if (compression == 2) {
					unpacked = new byte[bytesPerLine * height];
					VdatStream() rle;
					rle.Content = content;
					rle.ContentOffset = contentOffset + 8;
					for (int bitplane = 0; bitplane < bitplanes; bitplane++) {
						if (rle.ContentOffset + 14 > contentLength || !IsStringAt(content, rle.ContentOffset, "VDAT"))
							return false;
						int nextContentOffset = rle.ContentOffset + 8 + Get32BigEndian(content, rle.ContentOffset + 4);
						if (nextContentOffset > contentOffset + 8 + chunkLength)
							return false;
						rle.ValueOffset = rle.ContentLength = rle.ContentOffset + 8 + (content[rle.ContentOffset + 8] << 8) + content[rle.ContentOffset + 9];
						rle.ValueLength = nextContentOffset;
						rle.ContentOffset += 10;
						for (int x = bitplane << 1; x < bytesPerLine; x += bitplanes << 1) {
							int unpackedOffset = x;
							for (int y = 0; y < height; y++) {
								int b = rle.ReadRle();
								if (b < 0)
									return false;
								unpacked[unpackedOffset] = b >> 8;
								unpacked[unpackedOffset + 1] = b.LowByte;
								unpackedOffset += bytesPerLine;
							}
						}
						rle.ContentOffset = nextContentOffset;
					}
					SetSizeStOrFalcon(width, height, bitplanes);
				}
				else {
					PackBitsStream() rle;
					rle.Content = content;
					rle.ContentOffset = contentOffset + 8;
					rle.ContentLength = contentOffset + 8 + chunkLength;

					if (type == IffType.Pbm) {
						if (colors == 0)
							return false;
						SetSize(width, height, RECOILResolution.Amiga1x1);
						for (int y = 0; y < height; y++) {
							for (int x = 0; x < width; x++) {
								int b = compression == 0 ? rle.ReadByte() : rle.ReadRle();
								if (b < 0)
									return false;
								Pixels[y * width + x] = ContentPalette[b];
							}
						}
						return true;
					}

					unpacked = new byte[bytesPerLine * height];
					for (int y = 0; y < height; y++) {
						for (int bitplane = 0; bitplane < bitplanes; bitplane++) {
							for (int w = bitplane << 1; w < bytesPerLine; w += bitplanes << 1) {
								for (int x = 0; x < 2; x++) {
									int b = compression == 0 ? rle.ReadByte() : rle.ReadRle();
									if (b < 0)
										return false;
									unpacked[y * bytesPerLine + w + x] = b;
								}
							}
						}
						if (hasMask) {
							// ignore transparency mask
							for (int x = 0; x < width; x += 8) {
								int b = compression == 0 ? rle.ReadByte() : rle.ReadRle();
								if (b < 0)
									return false;
							}
						}
					}
					SetSize(width, height, RECOILResolution.Amiga1x1);
				}
				return DecodeIffUnpacked(unpacked, bitplanes, colors, camg);
			}
			else if (IsStringAt(content, contentOffset, "ABIT")) {
				if (width == 0 || compression != 0 || chunkLength != (width >> 3) * height * bitplanes)
					return false;
				contentOffset += 8;
				byte[] unpacked = new byte[chunkLength];
				for (int bitplane = 0; bitplane < bitplanes; bitplane++) {
					for (int unpackedOffset = bitplane << 1; unpackedOffset < chunkLength; unpackedOffset += bitplanes << 1) {
						for (int x = 0; x < 2; x++)
							unpacked[unpackedOffset + x] = content[contentOffset++];
					}
				}
				SetSize(width, height, RECOILResolution.Amiga1x1);
				return DecodeIffUnpacked(unpacked, bitplanes, colors, camg);
			}
			contentOffset += 8 + (chunkLength + 1 & ~1); // round up to even bytes
		}
		return false;
	}

	// Atari 8-bit formats.

	int[256] Atari8Palette;

	/// Sets Atari 8-bit palette from a 768-byte array (256 times RGB).
	public void SetAtari8Palette(byte[] content)
	{
		if (content == null)
			content = BinaryResource("altirrapal.pal");
		for (int i = 0; i < 256; i++)
			Atari8Palette[i] = content[i * 3] << 16 | content[i * 3 + 1] << 8 | content[i * 3 + 2];
	}

	static int ParseAtari8ExecutableHeader(byte[] content, int contentOffset)
	{
		if (content[contentOffset] != 0xff || content[contentOffset + 1] != 0xff)
			return -1;
		int startAddress = content[contentOffset + 2] | content[contentOffset + 3] << 8;
		int endAddress = content[contentOffset + 4] | content[contentOffset + 5] << 8;
		return endAddress - startAddress + 1;
	}

	static int GetAtari8ExecutableOffset(byte[] content, int contentLength)
	{
		if (contentLength >= 7) {
			int blockLength = ParseAtari8ExecutableHeader(content, 0);
			if (blockLength > 0 && 6 + blockLength == contentLength)
				return 6;
		}
		return 0;
	}

	bool SetAtari8RawSize(byte[] content, int contentLength, RECOILResolution resolution)
	{
		int contentOffset = GetAtari8ExecutableOffset(content, contentLength);
		int height = (contentLength - contentOffset) / 40;
		if (height == 0 || height > 240)
			return false;
		SetSize(320, height, resolution);
		return true;
	}

	byte[16] GtiaColors;

	void SetGtiaColor(int reg, int value)
	{
		value &= 0xfe;
		switch (reg) {
		case 0:
		case 1:
		case 2:
		case 3:
			GtiaColors[reg] = value;
			break;
		case 4:
		case 5:
		case 6:
		case 7:
			GtiaColors[8 + reg] = GtiaColors[reg] = value;
			break;
		case 8:
			GtiaColors[11] = GtiaColors[10] = GtiaColors[9] = GtiaColors[8] = value;
			break;
		}
	}

	void SetPM123PF0123Bak(byte[] content, int contentOffset)
	{
		for (int i = 0; i < 8; i++)
			SetGtiaColor(1 + i, content[contentOffset + i]);
	}

	void SetGtiaColors(byte[] content, int contentOffset)
	{
		GtiaColors[0] = content[contentOffset] & 0xfe;
		SetPM123PF0123Bak(content, contentOffset + 1);
	}

	void SetPF21(byte[] content, int contentOffset)
	{
		GtiaColors[6] = content[contentOffset] & 0xfe;
		GtiaColors[5] = content[contentOffset + 1] & 0xfe;
	}

	void SetGr15DefaultColors()
	{
		GtiaColors[8] = 0x00;
		GtiaColors[4] = 0x04;
		GtiaColors[5] = 0x08;
		GtiaColors[6] = 0x0c;
	}

	void SetBakPF012(byte[] content, int contentOffset, int contentStride)
	{
		for (int i = 0; i < 4; i++)
			GtiaColors[i == 0 ? 8 : 3 + i] = content[contentOffset + i * contentStride] & 0xfe;
	}

	void SetBakPF0123(byte[] content, int contentOffset)
	{
		for (int i = 0; i < 5; i++)
			GtiaColors[i == 0 ? 8 : 3 + i] = content[contentOffset + i] & 0xfe;
	}

	void SetPF012Bak(byte[] content, int contentOffset)
	{
		for (int i = 0; i < 4; i++)
			GtiaColors[i == 3 ? 8 : 4 + i] = content[contentOffset + i] & 0xfe;
	}

	void SetPF0123Bak(byte[] content, int contentOffset)
	{
		for (int i = 0; i < 5; i++)
			GtiaColors[4 + i] = content[contentOffset + i] & 0xfe;
	}

	void SetPF0123Even(byte[] content, int contentOffset)
	{
		for (int i = 0; i < 4; i++)
			GtiaColors[4 + i] = content[contentOffset + i * 2] & 0xfe;
	}

	void DecodeAtari8Gr8(byte[] content, int contentOffset, byte[] frame, int frameOffset, int height)
	{
		byte[2] colors;
		colors[0] = GtiaColors[6];
		colors[1] = GtiaColors[6] & 0xf0 | GtiaColors[5] & 0x0e;
		frameOffset -= LeftSkip;
		for (int y = 0; y < height; y++) {
			int x;
			for (x = LeftSkip; x < Width; x++) {
				int c = content[contentOffset + (x >> 3)] >> (~x & 7) & 1;
				frame[frameOffset + x] = colors[c];
			}
			for ( ; x < Width + LeftSkip; x++)
				frame[frameOffset + x] = GtiaColors[8];
			contentOffset += Width >> 3;
			frameOffset += Width;
		}
	}

	void DecodeAtari8Gr15(byte[] content, int contentOffset, int contentStride, byte[] frame, int frameOffset, int frameStride, int height)
	{
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < Width; x++) {
				int c = content[contentOffset + (x >> 3)] >> (~x & 6) & 3;
				frame[frameOffset + x] = GtiaColors[c == 0 ? 8 : c + 3];
			}
			contentOffset += contentStride;
			frameOffset += frameStride;
		}
	}

	void DecodeAtari8Gr7(byte[] content, int contentOffset, byte[] frame, int frameOffset, int height)
	{
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < Width; x++) {
				int c = content[contentOffset + (x >> 3)] >> (~x & 6) & 3;
				frame[frameOffset + x + Width] = frame[frameOffset + x] = GtiaColors[c == 0 ? 8 : c + 3];
			}
			contentOffset += Width >> 3;
			frameOffset += Width << 1;
		}
	}

	void DecodeAtari8Gr9(byte[] content, int contentOffset, int contentStride, byte[] frame, int frameOffset, int frameStride, int width, int height)
	{
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				int i = x + LeftSkip;
				int c = i < 0 || i >= width ? 0 : content[contentOffset + (i >> 3)] >> (~i & 4) & 0x0f;
				frame[frameOffset + x] = GtiaColors[8] | c;
			}
			contentOffset += contentStride;
			frameOffset += frameStride;
		}
	}

	void DecodeAtari8Gr11(byte[] content, int contentOffset, byte[] frame, int frameOffset, int frameStride, int height)
	{
		frameOffset -= LeftSkip;
		for (int y = 0; y < height; y++) {
			int x;
			for (x = LeftSkip; x < Width; x++) {
				int c = content[contentOffset + (x >> 3)] << (x & 4) & 0xf0;
				c = c == 0 ? GtiaColors[8] & 0xf0 : GtiaColors[8] | c;
				frame[frameOffset + x] = c;
			}
			for ( ; x < Width + LeftSkip; x++)
				frame[frameOffset + x] = GtiaColors[8] & 0xf0;
			contentOffset += Width >> 3;
			frameOffset += frameStride;
		}
	}

	void DecodeAtari8Gr10(byte[] content, int contentOffset, byte[] frame, int frameOffset, int frameStride, int height)
	{
		frameOffset += 2 - LeftSkip;
		for (int y = 0; y < height; y++) {
			int x;
			for (x = LeftSkip - 2; x < 0; x++)
				frame[frameOffset + x] = GtiaColors[0];
			for ( ; x < Width + LeftSkip - 2; x++) {
				int c = content[contentOffset + (x >> 3)] >> (~x & 4) & 0x0f;
				frame[frameOffset + x] = GtiaColors[c];
			}
			contentOffset += Width >> 3;
			frameOffset += frameStride;
		}
	}

	void DecodeAtari8Gr11PalBlend(byte[] content, int contentOffset, int contentStride, byte[] frame, int y)
	{
		for ( ; y < Height; y += 2) {
			int frameOffset = y * Width - LeftSkip;
			int x;
			for (x = LeftSkip; x < Width; x++) {
				int c = content[contentOffset + (x >> 3)] << (x & 4) & 0xf0;
				// Average intensity from neighboring lines. This doesn't happen on real hardware,
				// but avoids ugly aliasing with non-integer zoom factors.
				int i =
					((y == 0 ? 0 : frame[frameOffset - Width + x] & 0x0f) +
					(y == Height - 1 ? 0 : frame[frameOffset + Width + x] & 0x0f)) >> 1;
				frame[frameOffset + x] = c | i;
				// Copy hue to the next line emulating PAL color resolution reduction.
				// TODO: we should combine the two hues.
				if (y < Height - 1)
					frame[frameOffset + Width + x] = c | (frame[frameOffset + Width + x] & 0x0f);
			}
			for ( ; x < Width + LeftSkip; x++)
				frame[frameOffset + x] = 0;
			contentOffset += contentStride;
		}
	}

	static int ToAtari8Char(int ascii)
	{
		switch (ascii & 0x60) {
		case 0x00:
			return ascii + 0x40;
		case 0x20:
		case 0x40:
			return ascii - 0x20;
		default:
			return ascii;
		}
	}

	void DecodeAtari8Gr0(byte[] characters, int charactersStride, byte[] font, int fontOffset, byte[] frame)
	{
		for (int y = 0; y < Height; y++) {
			for (int x = 0; x < Width; x++) {
				int ch = characters[(y >> 3) * charactersStride + (x >> 3)];
				int b = font[fontOffset + ((ch & 0x7f) << 3) + (y & 7)];
				if (ch >= 0x80)
					b ^= 0xff;
				frame[y * Width + x] = (b >> (~x & 7) & 1) == 0 ? 0x00 : 0x0e;
			}
		}
	}

	void DecodeAtari8Gr12Line(byte[] characters, int charactersOffset, byte[] font, int fontOffset, byte[] frame, int frameOffset)
	{
		for (int y = 0; y < 8; y++) {
			for (int x = 0; x < Width; x++) {
				int ch = characters[charactersOffset + (x >> 3)];
				int c = font[fontOffset + ((ch & 0x7f) << 3) + y] >> (~x & 6) & 3;
				int gr12Registers = ch >= 0x80 ? 0x7548 : 0x6548;
				frame[frameOffset + x] = GtiaColors[gr12Registers >> (c << 2) & 0xf];
			}
			frameOffset += Width;
		}
	}

	void DecodeAtari8Player(byte[] content, int contentOffset, int color, byte[] frame, int frameOffset, int height)
	{
		color &= 0xfe;
		for (int y = 0; y < height; y++) {
			int b = content[contentOffset + y];
			for (int x = 0; x < 8; x++) {
				int c = b >> (7 - x) & 1;
				if (c != 0)
					frame[frameOffset + x * 2 + 1] = frame[frameOffset + x * 2] |= color;
			}
			frameOffset += Width;
		}
	}

	bool ApplyAtari8Palette(byte[] frame)
	{
		int pixelsLength = Width * Height;
		for (int i = 0; i < pixelsLength; i++)
			Pixels[i] = Atari8Palette[frame[i]];
		return true;
	}

	bool ApplyAtari8PaletteBlend(byte[] frame1, byte[] frame2)
	{
		int pixelsLength = Width * Height;
		Frames = 2;
		for (int i = 0; i < pixelsLength; i++) {
			int rgb1 = Atari8Palette[frame1[i]];
			int rgb2 = Atari8Palette[frame2[i]];
			// This clever formula just computes the byte-by-byte averages.
			Pixels[i] = (rgb1 & rgb2) + ((rgb1 ^ rgb2) >> 1 & 0x7f7f7f);
		}
		return true;
	}

	bool ApplyAtari8PaletteBlend3(byte[] frame1, byte[] frame2, byte[] frame3)
	{
		int pixelsLength = Width * Height;
		Frames = 3;
		for (int i = 0; i < pixelsLength; i++) {
			int rgb1 = Atari8Palette[frame1[i]];
			int rgb2 = Atari8Palette[frame2[i]];
			int rgb3 = Atari8Palette[frame3[i]];
			Pixels[i] = ((rgb1 >> 16) + (rgb2 >> 16) + (rgb3 >> 16)) / 3 << 16
				| ((rgb1 >> 8 & 0xff) + (rgb2 >> 8 & 0xff) + (rgb3 >> 8 & 0xff)) / 3 << 8
				| ((rgb1 & 0xff) + (rgb2 & 0xff) + (rgb3 & 0xff)) / 3;
		}
		return true;
	}

	bool DecodeGr8(byte[] content, int contentLength)
	{
		if (!SetAtari8RawSize(content, contentLength, RECOILResolution.Xe1x1))
			return false;
		GtiaColors[6] = 0x00;
		GtiaColors[5] = 0x0e;
		int contentOffset = GetAtari8ExecutableOffset(content, contentLength);
		byte[320 * 240] frame;
		DecodeAtari8Gr8(content, contentOffset, frame, 0, Height);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeDrg(byte[] content, int contentLength)
		return contentLength == 6400 && DecodeGr8(content, contentLength);

	bool DecodeMbg(byte[] content, int contentLength)
	{
		if (contentLength != 16384)
			return false;
		SetSize(512, 256, RECOILResolution.Xe1x1);
		GtiaColors[6] = 0x00;
		GtiaColors[5] = 0x0e;
		byte[512 * 256] frame;
		DecodeAtari8Gr8(content, 0, frame, 0, 256);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeGhg(byte[] content, int contentLength)
	{
		if (contentLength < 4)
			return false;
		int width = ((content[0] | content[1] << 8) + 7) & ~7; // round up to 8 pixels
		int height = content[2];
		if (width == 0 || width > 320 || height == 0 || height > 200)
			return false;
		SetSize(width, height, RECOILResolution.Xe1x1);
		byte[320 * 200] frame;
		GtiaColors[6] = 0x0c;
		GtiaColors[5] = 0x02;
		DecodeAtari8Gr8(content, 3, frame, 0, height);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeCpr(byte[] content, int contentLength)
	{
		if (contentLength < 2)
			return false;
		byte[7680] unpacked;
		if (!XeKoalaStream.UnpackRaw(content[0], content, 1, contentLength, unpacked, 7680))
			return false;
		GtiaColors[6] = 0x0c;
		GtiaColors[5] = 0x00;
		SetSize(320, 192, RECOILResolution.Xe1x1);
		byte[320 * 192] frame;
		DecodeAtari8Gr8(unpacked, 0, frame, 0, 192);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeGr7(byte[] content, int contentOffset, int contentSize)
	{
		if (contentSize > 4804 || contentSize % 40 != 4)
			return false;
		int height = contentSize / 40;
		SetSize(320, height * 2, RECOILResolution.Xe2x2);
		byte[320 * 240] frame;
		SetBakPF012(content, contentOffset + contentSize - 4, 1);
		DecodeAtari8Gr7(content, contentOffset, frame, 0, height);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeRys(byte[] content, int contentLength)
	{
		if (contentLength != 3840)
			return false;
		SetSize(320, 192, RECOILResolution.Xe2x2);
		byte[320 * 192] frame;
		GtiaColors[8] = 0x00;
		GtiaColors[4] = 0x28;
		GtiaColors[5] = 0xca;
		GtiaColors[6] = 0x94;
		DecodeAtari8Gr7(content, 0, frame, 0, 96);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeBkg(byte[] content, int contentLength)
		return contentLength == 3856 && DecodeGr7(content, 0, 3844);

	bool DecodeAtari8Artist(byte[] content, int contentLength)
	{
		if (contentLength != 3206 || content[0] != 7)
			return false;
		SetSize(320, 160, RECOILResolution.Xe2x2);
		byte[320 * 160] frame;
		SetPF0123Bak(content, 1);
		DecodeAtari8Gr7(content, 6, frame, 0, 80);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeGr9(byte[] content, int contentLength)
	{
		if (!SetAtari8RawSize(content, contentLength, RECOILResolution.Xe4x1))
			return false;
		GtiaColors[8] = 0x00;
		int contentOffset = GetAtari8ExecutableOffset(content, contentLength);
		byte[320 * 240] frame;
		DecodeAtari8Gr9(content, contentOffset, 40, frame, 0, 320, 320, Height);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeA4r(byte[] content, int contentLength)
	{
		A4rStream() a4r;
		a4r.Content = content;
		a4r.ContentOffset = 0;
		a4r.ContentLength = contentLength;
		if (!a4r.UnpackA4r())
			return false;
		SetSize(320, 256, RECOILResolution.Xe4x1);
		byte[320 * 256] frame;
		GtiaColors[8] = 0x00;
		DecodeAtari8Gr9(a4r.Unpacked, 0x5010 - A4rStream.MinAddress, 40, frame, 0, 320, 320, 256);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeG11(byte[] content, int contentLength)
	{
		if (!SetAtari8RawSize(content, contentLength, RECOILResolution.Xe4x1))
			return false;
		GtiaColors[8] = 0x06; // Atari operating system standard
		int contentOffset = GetAtari8ExecutableOffset(content, contentLength);
		byte[320 * 240] frame;
		DecodeAtari8Gr11(content, contentOffset, frame, 0, 320, Height);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeG10(byte[] content, int contentLength)
	{
		if (!SetAtari8RawSize(content, contentLength, RECOILResolution.Xe4x1))
			return false;
		int contentOffset = GetAtari8ExecutableOffset(content, contentLength);
		if ((contentLength - contentOffset) % 40 != 9)
			return false;
		LeftSkip = 2;
		SetGtiaColors(content, contentLength - 9);
		byte[320 * 240] frame;
		DecodeAtari8Gr10(content, contentOffset, frame, 0, 320, Height);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeG09(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 7680:
			return DecodeGr9(content, contentLength);
		case 15360:
			break;
		default:
			return false;
		}
		SetSize(640, 192, RECOILResolution.Xe4x1);
		GtiaColors[8] = 0x00;
		byte[640 * 192] frame;
		DecodeAtari8Gr9(content, 0, 40, frame, 0, 640, 320, 192);
		DecodeAtari8Gr9(content, 7680, 40, frame, 320, 640, 320, 192);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeMic(byte[] content, int contentLength)
	{
		if (contentLength == 15872) {
			// AtariGraphics file format.
			// AtariGraphics was included on a cartridge with the Atari Light Pen.
			// Its files have no default extension. I chose MIC, because it's similar.
			// The format is: 7680 bytes picture, followed by PF0,PF1,PF2,BAK colors,
			// followed by 508 bytes of padding, followed by 7680 bytes of fill map.
			// The fill map allows flood filling areas previously filled with a pattern.
			// It is a copy of the picture, except that areas filled with a pattern
			// are filled with a solid color here. Also, the fill map has inverted bits
			// compared to the picture, don't know why.
			contentLength = 7680;
			SetPF012Bak(content, 7680);
		}
		else {
			switch (contentLength % 40) {
			case 0:
			case 3: // I've found 7683-byte PIC files with three zero bytes at the end.
				SetGr15DefaultColors();
				break;
			case 4:
				SetBakPF012(content, contentLength - 4, 1);
				break;
			case 5:
				// Probably the last byte should be taken for COLBAK, not the previous one,
				// but I can't check that since the 7685-byte PIC files I found
				// have two zero bytes at the end.
				SetPF012Bak(content, contentLength - 5);
				break;
			default:
				return false;
			}
		}

		int height = contentLength / 40;
		if (height == 0 || height > 240)
			return false;
		SetSize(320, height, RECOILResolution.Xe2x1);
		byte[320 * 240] frame;
		DecodeAtari8Gr15(content, 0, 40, frame, 0, 320, height);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeHpm(byte[] content, int contentLength)
	{
		// This format appears in Grass' Slideshow.
		byte[7684] unpacked;
		HpmStream() rle;
		rle.Content = content;
		rle.ContentOffset = 0;
		rle.ContentLength = contentLength;
		if (!rle.Unpack(unpacked, 0, 1, 7680))
			return false;

		// Colors aren't stored in the file.
		// Yvonne (http://atari8.sourceforge.net) sets colors according to filenames
		// (note Grass' Slideshow and Yvonne were programmed by the same guy).
		// I have found out that the last byte of the file is COLPF0.
		// Other colors can be deduced from this one.
		// One exception is JORDAN.HPM, which I additionally match by length (3494 bytes).
		switch (rle.ReadByte()) {
		case 0x34: // ALIEN.HPM, DN.HPM, HPZ.HPM, RAPER.HPM, SALEM.HPM, ZWIEWKA.HPM, JORDAN.HPM
		case 0x35: // KOPALNY.HPM
			unpacked[7680] = 0x00;
			unpacked[7681] = 0x34;
			unpacked[7682] = contentLength == 3494 ? 0x38 : 0xc8;
			unpacked[7683] = contentLength == 3494 ? 0x3c : 0x7c;
			break;
		case 0x51: // CZASZKA.HPM
			unpacked[7680] = 0xa4;
			unpacked[7681] = 0x51;
			unpacked[7682] = 0xb9;
			unpacked[7683] = 0x7c;
			break;
		case 0xe4: // FSILY.HPM
			unpacked[7680] = 0x00;
			unpacked[7681] = 0xe4;
			unpacked[7682] = 0xc8;
			unpacked[7683] = 0xbe;
			break;
		case 0x04: // GIRL.HPM
			unpacked[7680] = 0x06;
			unpacked[7681] = 0x04;
			unpacked[7682] = 0x00;
			unpacked[7683] = 0x0a;
			break;
		case 0x30: // KISS.HPM
			unpacked[7680] = 0x0e;
			unpacked[7681] = 0x30;
			unpacked[7682] = 0xc7;
			unpacked[7683] = 0x7b;
			break;
		case 0x74: // STAR.HPM, VIVALDI.HPM
			unpacked[7680] = 0x00;
			unpacked[7681] = 0x74; // for STAR.HPM Yvonne uses 0x64, unlike the slideshow
			unpacked[7682] = 0x58;
			unpacked[7683] = 0x7e;
			break;
		// case 0x05: // FATHER.HPM
		default:
			unpacked[7680] = 0x00;
			unpacked[7681] = 0x04;
			unpacked[7682] = 0x08;
			unpacked[7683] = 0x0c;
			break;
		}

		return DecodeMic(unpacked, 7684);
	}

	bool DecodePic(byte[] content, int contentLength)
	{
		byte[7680] unpacked;
		if (XeKoalaStream.UnpackWrapped(content, contentLength, unpacked, 7680)) {
			SetSize(320, 192, RECOILResolution.Xe2x1);
			SetPF0123Bak(content, 13);
			byte[320 * 192] frame;
			DecodeAtari8Gr15(unpacked, 0, 40, frame, 0, 320, 192);
			return ApplyAtari8Palette(frame);
		}

		// Some images with .PIC extension are GR8.
		if (contentLength == 7680)
			return DecodeGr8(content, contentLength);

		// Some images with .PIC extension are MIC.
		if (contentLength >= 7681 && contentLength <= 7685)
			return DecodeMic(content, contentLength);

		// Some images with .PIC extension are DOO or SC8.
		return DecodeDoo(content, contentLength)
			|| DecodeSc8(content, contentLength);
	}

	bool DecodeWnd(byte[] content, int contentLength)
	{
		if (contentLength != 3072)
			return false;
		// first byte = width in pixels minus one
		// round up to 4 pixels
		int contentStride = (content[0] >> 2) + 1;
		int height = content[1];
		if (contentStride > 40 || height == 0 || height > 192 || contentStride * height > 3070)
			return false;
		SetSize(contentStride << 3, height, RECOILResolution.Xe2x1);
		byte[320 * 192] frame;
		// Blazing Paddles default colors
		GtiaColors[8] = 0x00;
		GtiaColors[4] = 0x46;
		GtiaColors[5] = 0x88;
		GtiaColors[6] = 0x0e;
		DecodeAtari8Gr15(content, 2, contentStride, frame, 0, contentStride << 3, height);
		return ApplyAtari8Palette(frame);
	}

	const int At800SpriteGap = 2;

	void DecodeAt800Players(byte[] content, byte[] frame)
	{
		for (int i = 0; i < 4; i++)
			DecodeAtari8Player(content, 4 + i * 240, content[i], frame, i * (8 + At800SpriteGap) * 2, 240);
	}

	void DecodeAt800Missiles(byte[] content, int contentOffset, byte[] frame, int frameOffset)
	{
		for (int y = 0; y < 240; y++) {
			for (int i = 0; i < 4; i++) {
				int b = content[contentOffset + y] >> (i << 1);
				int offset = frameOffset + i * (2 + At800SpriteGap) * 2;
				frame[offset + 1] = frame[offset] = (b & 2) == 0 ? 0 : content[i];
				frame[offset + 3] = frame[offset + 2] = (b & 1) == 0 ? 0 : content[i];
			}
			frameOffset += Width;
		}
	}

	bool DecodePla(byte[] content, int contentLength)
	{
		if (contentLength != 241)
			return false;
		SetSize(16, 240, RECOILResolution.Xe2x1);
		byte[16 * 240] frame = 0;
		DecodeAtari8Player(content, 1, content[0], frame, 0, 240);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeMis(byte[] content, int contentLength)
	{
		if (contentLength != 61 && contentLength != 241)
			return false;
		const int width = 2 * 2;
		SetSize(width, 240, RECOILResolution.Xe2x1);
		byte[width * 240] frame;
		for (int y = 0; y < 240; y++) {
			int b = content[1 + (y >> 2)] >> ((~y & 3) << 1);
			frame[y * width + 1] = frame[y * width] = (b & 2) == 0 ? 0 : content[0];
			frame[y * width + 3] = frame[y * width + 2] = (b & 1) == 0 ? 0 : content[0];
		}
		return ApplyAtari8Palette(frame);
	}

	bool Decode4pl(byte[] content, int contentLength)
	{
		if (contentLength != 964)
			return false;
		const int width = 4 * (8 + At800SpriteGap) * 2;
		SetSize(width, 240, RECOILResolution.Xe2x1);
		byte[width * 240] frame = 0;
		DecodeAt800Players(content, frame);
		return ApplyAtari8Palette(frame);
	}

	bool Decode4mi(byte[] content, int contentLength)
	{
		if (contentLength != 244)
			return false;
		const int width = 4 * (2 + At800SpriteGap) * 2;
		SetSize(width, 240, RECOILResolution.Xe2x1);
		byte[width * 240] frame = 0;
		DecodeAt800Missiles(content, 4, frame, 0);
		return ApplyAtari8Palette(frame);
	}

	bool Decode4pm(byte[] content, int contentLength)
	{
		if (contentLength != 1204)
			return false;
		const int width = 4 * (8 + At800SpriteGap + 2 + At800SpriteGap) * 2;
		SetSize(width, 240, RECOILResolution.Xe2x1);
		byte[width * 240] frame = 0;
		DecodeAt800Players(content, frame);
		DecodeAt800Missiles(content, 964, frame, 4 * (8 + At800SpriteGap) * 2);
		return ApplyAtari8Palette(frame);
	}

	bool DecodePmd(byte[] content, int contentLength)
	{
		if (contentLength < 3 + 4 + 4 + 1
		 || content[0] != 0xf0 || content[1] != 0xed || content[2] != 0xe4)
			return false;
		int sprites = content[7];
		int shapes = content[8] * content[9];
		int totalShapes = sprites * shapes;
		int height = content[10];
		if (sprites == 0 || sprites > 4
		 || shapes == 0 || shapes > 16 * 10
		 || height == 0 || height > 48
		 || 11 + totalShapes * height != contentLength)
			return false;

		// Unfortunately the information whether sprites are multi-color is not saved in the file.
		// We choose multi because there are three multi examples vs one mono.
		const bool multi = true;
		if (multi)
			totalShapes >>= 1;
		// 16 shapes per row * (8 pixels + 2 pixels gap) = 160 pixels wide
		int rows = (totalShapes + 15 >> 4);
		const int horizontalGap = 2;
		const int verticalGap = 2;
		const int maxHeight = 560;
		if (rows == 1)
			SetSize(totalShapes * (8 + horizontalGap << 1), height, RECOILResolution.Xe2x1);
		else {
			int totalHeight = rows * height + (rows - 1) * verticalGap;
			if (totalHeight > maxHeight)
				return false;
			SetSize(320, totalHeight, RECOILResolution.Xe2x1);
		}

		byte[320 * maxHeight] frame = 0;
		for (int shape = 0; shape < totalShapes; shape++) {
			int frameOffset = (shape >> 4) * (height + verticalGap) * 320 + (shape & 0xf) * (8 + horizontalGap << 1);
			if (multi) {
				int spritePair = shape / shapes;
				int contentOffset = 11 + (spritePair * shapes + shape) * height;
				DecodeAtari8Player(content, contentOffset, content[3 + spritePair * 2], frame, frameOffset, height);
				DecodeAtari8Player(content, contentOffset + shapes * height, content[4 + spritePair * 2], frame, frameOffset, height);
			}
			else
				DecodeAtari8Player(content, 11 + shape * height, content[3 + shape / shapes], frame, frameOffset, height);
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeApl(byte[] content, int contentLength)
	{
		if (contentLength != 1677
		 || content[0] != 0x9a || content[1] != 0xf8 || content[2] != 0x39 || content[3] != 0x21)
			return false;
		int frames = content[4];
		int height = content[5];
		int gap = content[6];
		if (frames == 0 || frames > 16 || height == 0 || height > 48 || gap > 8)
			return false;
		const int frameGap = 2;
		int frameWidth = 8 + gap + frameGap << 1;
		SetSize(frames * frameWidth, height, RECOILResolution.Xe2x1);
		byte[(16 + frameGap) * 2 * 16 * 48] frame = 0;

		for (int f = 0; f < frames; f++) {
			DecodeAtari8Player(content, 42 + f * 48, content[7 + f], frame, f * frameWidth, height);
			DecodeAtari8Player(content, 858 + f * 48, content[24 + f], frame, f * frameWidth + gap * 2, height);
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeHr(byte[] content, int contentLength)
	{
		if (contentLength != 16384)
			return false;
		SetSize(256, 239, RECOILResolution.Xe1x1);
		GtiaColors[6] = 0x00;
		GtiaColors[5] = 0x0e;
		byte[256 * 239] frame1;
		DecodeAtari8Gr8(content, 0, frame1, 0, 239);
		byte[256 * 239] frame2;
		DecodeAtari8Gr8(content, 8192, frame2, 0, 239);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeMcpp(byte[] content, int contentLength)
	{
		if (contentLength != 8008)
			return false;
		SetSize(320, 200, RECOILResolution.Xe2x2);
		byte[320 * 200] frame;
		SetPF012Bak(content, 8000);
		DecodeAtari8Gr15(content, 0, 40, frame, 0, 640, 100);
		SetPF012Bak(content, 8004);
		DecodeAtari8Gr15(content, 4000, 40, frame, 320, 640, 100);
		return ApplyAtari8Palette(frame);
	}

	// INP, ING
	bool DecodeInp(byte[] content, int contentLength)
	{
		if (contentLength < 16004)
			return false;
		SetSize(320, 200, RECOILResolution.Xe2x1);
		SetBakPF012(content, 16000, 1);
		byte[320 * 200] frame1;
		DecodeAtari8Gr15(content, 0, 40, frame1, 0, 320, 200);
		byte[320 * 200] frame2;
		DecodeAtari8Gr15(content, 8000, 40, frame2, 0, 320, 200);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeIge(byte[] content, int contentLength)
	{
		if (contentLength != 6160
		 || content[0] != 0xff || content[1] != 0xff
		 || content[2] != 0xf6 || content[3] != 0xa3 || content[4] != 0xff || content[5] != 0xbb
		 || content[6] != 0xff || content[7] != 0x5f)
			return false;
		SetSize(256, 96, RECOILResolution.Xe2x1);
		byte[256 * 96] frame1;
		SetBakPF012(content, 8, 1);
		DecodeAtari8Gr15(content, 0x10, 32, frame1, 0, 256, 96);
		byte[256 * 96] frame2;
		SetBakPF012(content, 12, 1);
		DecodeAtari8Gr15(content, 0xc10, 32, frame2, 0, 256, 96);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeInt(byte[] content, int contentLength)
	{
		if (contentLength < 18
		 || !IsStringAt(content, 0, "INT95a")
		 || content[8] != 0x0f || content[9] != 0x2b)
			return false;
		int contentStride = content[6];
		int height = content[7];
		if (contentStride == 0 || contentStride > 320
		 || height == 0 || height > 239
		 || 18 + contentStride * height * 2 != contentLength)
			return false;
		int width = contentStride << 3;
		SetSize(width, height, RECOILResolution.Xe2x1);
		byte[320 * 239] frame1;
		SetBakPF012(content, 10, 1);
		DecodeAtari8Gr15(content, 18, contentStride, frame1, 0, width, height);
		byte[320 * 239] frame2;
		SetBakPF012(content, 14, 1);
		DecodeAtari8Gr15(content, 18 + contentStride * height, contentStride, frame2, 0, width, height);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeIst(byte[] content, int contentLength)
	{
		if (contentLength != 17184)
			return false;
		SetSize(320, 200, RECOILResolution.Xe2x1);
		byte[320 * 200] frame1;
		byte[320 * 200] frame2;
		for (int y = 0; y < 200; y++) {
			SetBakPF012(content, 0x4000 + y, 200);
			DecodeAtari8Gr15(content, 16 + y * 40, 0, frame1, y * 320, 320, 1);
			DecodeAtari8Gr15(content, 0x2010 + y * 40, 0, frame2, y * 320, 320, 1);
		}
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeGr15Blend(byte[] content, int bitmapOffset, int colorsOffset, int height)
	{
		SetSize(320, height, RECOILResolution.Xe2x1);
		byte[320 * 200] frame1;
		SetPF012Bak(content, colorsOffset);
		DecodeAtari8Gr15(content, bitmapOffset, 80, frame1, 0, 640, height >> 1);
		SetPF012Bak(content, colorsOffset + 4);
		DecodeAtari8Gr15(content, bitmapOffset + 40, 80, frame1, 320, 640, height >> 1);
		byte[320 * 200] frame2;
		DecodeAtari8Gr15(content, bitmapOffset + height * 40, 80, frame2, 0, 640, height >> 1);
		SetPF012Bak(content, colorsOffset);
		DecodeAtari8Gr15(content, bitmapOffset + height * 40 + 40, 80, frame2, 320, 640, height >> 1);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeMcp(byte[] content, int contentLength)
	{
		if (contentLength != 16008)
			return false;
		return DecodeGr15Blend(content, 0, 16000, 200);
	}

	bool DecodeRaw(byte[] content, int contentLength)
	{
		if (contentLength != 15372 || !IsStringAt(content, 0, "XLPB"))
			return false;
		return DecodeGr15Blend(content, 4, 0x3c04, 192);
	}

	bool DecodeXlp(byte[] content, int contentLength)
	{
		XlpStream() rle;
		rle.Content = content;
		rle.ContentLength = contentLength;
		byte[16000] unpacked;
		int height;
		int colorsOffset;
		if (contentLength >= 10 && IsStringAt(content, 0, "XLPC")) {
			// Ignore errors: STAIRS.XLP included with XL-Paint is missing last two image bytes.
			unpacked.Clear();
			rle.ContentOffset = 8;
			rle.Unpack(unpacked, 0, 40, 2 * 40 * 192);
			height = 192;
			colorsOffset = 4;
		}
		else {
			// No header so better check for errors.
			rle.ContentOffset = 4;
			if (rle.Unpack(unpacked, 0, 40, 2 * 40 * 200))
				height = 200;
			else {
				rle.ContentOffset = 4;
				if (rle.Unpack(unpacked, 0, 40, 2 * 40 * 192))
					height = 192;
				else
					return false;
			}
			colorsOffset = 0;
		}

		SetSize(320, height, RECOILResolution.Xe2x1);
		SetPF012Bak(content, colorsOffset);
		byte[320 * 200] frame1;
		DecodeAtari8Gr15(unpacked, 0, 40, frame1, 0, 320, height);
		byte[320 * 200] frame2;
		DecodeAtari8Gr15(unpacked, height * 40, 40, frame2, 0, 320, height);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeMax(byte[] content, int contentLength)
	{
		if (contentLength < 4 + 9 * 192 || !IsStringAt(content, 0, "XLPM"))
			return false;
		XlpStream() rle;
		rle.Content = content;
		rle.ContentOffset = 4 + 9 * 192;
		rle.ContentLength = contentLength;
		byte[15360] unpacked;
		if (!rle.Unpack(unpacked, 0, 40, 15360))
			return false;

		SetSize(320, 192, RECOILResolution.Xe2x1);
		byte[320 * 192] frame1;
		byte[320 * 192] frame2;
		for (int y = 0; y < 192; y++) {
			SetBakPF012(content, 4 + 4 * 192 + y, 192);
			DecodeAtari8Gr15(unpacked, y * 40, 40, frame1, y * 320, 320, 1);
			SetBakPF012(content, 4 + y, 192);
			DecodeAtari8Gr15(unpacked, 7680 + y * 40, 40, frame2, y * 320, 320, 1);
		}
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeHr2(byte[] content, int contentLength)
	{
		if (contentLength != 16006)
			return false;
		SetSize(320, 200, RECOILResolution.Xe1x1);
		byte[320 * 200] frame1;
		SetPF21(content, 16000);
		DecodeAtari8Gr8(content, 0, frame1, 0, 200);
		byte[320 * 200] frame2;
		SetBakPF012(content, 16002, 1);
		DecodeAtari8Gr15(content, 8000, 40, frame2, 0, 320, 200);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	// LUM file combined with COL file gives 256-color Technicolor Dream.
	// Since we don't support decoding pairs of files, let's at least show the grayscale.
	bool DecodeLum(byte[] content, int contentLength)
	{
		if (contentLength != 4766)
			return false;
		SetSize(320, 192, RECOILResolution.Xe4x2);
		byte[320 * 192] frame;
		GtiaColors[8] = 0x00;
		DecodeAtari8Gr9(content, 6, 40, frame, 0, 640, 320, 96);
		DecodeAtari8Gr9(content, 6, 40, frame, 320, 640, 320, 96);
		return ApplyAtari8Palette(frame);
	}

	// APC, PLM
	bool DecodeApc(byte[] content, int contentLength)
	{
		if (contentLength != 7680 && contentLength != 7720)
			return false;
		SetSize(320, 192, RECOILResolution.Xe4x2);
		byte[320 * 192] frame;
		GtiaColors[8] = 0x00;
		DecodeAtari8Gr9(content, 40, 80, frame, 320, 640, 320, 96);
		DecodeAtari8Gr11PalBlend(content, 0, 80, frame, 0);
		return ApplyAtari8Palette(frame);
	}

	// 256, AP2
	bool Decode256(byte[] content, int contentLength)
	{
		if (contentLength != 7680 && contentLength != 7684)
			return false;
		SetSize(320, 192, RECOILResolution.Xe4x2);
		byte[320 * 192] frame;
		GtiaColors[8] = 0x00;
		DecodeAtari8Gr9(content, 3840, 40, frame, 320, 640, 320, 96);
		DecodeAtari8Gr11PalBlend(content, 0, 40, frame, 0);
		return ApplyAtari8Palette(frame);
	}

	// AP3, APV, DGP, ESC, ILC, PZM
	bool DecodeAp3(byte[] content, int contentLength)
	{
		int gr11Offset;
		switch (contentLength) {
		case 15360:
		case 15362:
			gr11Offset = 7680;
			break;
		case 15872:
			gr11Offset = 8192;
			break;
		default:
			return false;
		}
		SetSize(320, 192, RECOILResolution.Xe4x1);
		GtiaColors[8] = 0x00;
		byte[320 * 192] frame1;
		DecodeAtari8Gr9(content, 0, 80, frame1, 0, 640, 320, 96);
		DecodeAtari8Gr11PalBlend(content, gr11Offset + 40, 80, frame1, 1);
		byte[320 * 192] frame2;
		DecodeAtari8Gr9(content, 40, 80, frame2, 320, 640, 320, 96);
		DecodeAtari8Gr11PalBlend(content, gr11Offset, 80, frame2, 0);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeBgp(byte[] content, int contentLength)
	{
		if (contentLength < 0x27 + (2 + 239 * 40) * 2
		 || !IsStringAt(content, 0, "BUGBITER_APAC239I_PICTURE_V1.0")
		 || content[0x1e] != 0xff || content[0x1f] != 80 || content[0x20] != 239)
			return false;
		int textLength = content[0x25] + (content[0x26] << 8);
		// 0x2558 == 239 * 40
		if (contentLength != 0x27 + (2 + 239 * 40) * 2 + textLength
		 || content[0x27 + textLength] != 0x58 || content[0x28 + textLength] != 0x25
		 || content[0x27 + 2 + 239 * 40 + textLength] != 0x58 || content[0x28 + 2 + 239 * 40 + textLength] != 0x25)
			return false;
		SetSize(320, 239, RECOILResolution.Xe4x1);
		GtiaColors[8] = 0x00;
		byte[320 * 239] frame1;
		DecodeAtari8Gr9(content, 0x29 + textLength, 80, frame1, 0, 640, 320, 120);
		DecodeAtari8Gr11PalBlend(content, 0x29 + 239 * 40 + 2 + 40 + textLength, 80, frame1, 1);
		byte[320 * 239] frame2;
		DecodeAtari8Gr9(content, 0x29 + 40 + textLength, 80, frame2, 320, 640, 320, 119);
		DecodeAtari8Gr11PalBlend(content, 0x29 + 239 * 40 + 2 + textLength, 80, frame2, 0);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeApp(byte[] content, int contentLength)
	{
		if (contentLength < 22 + 15872 / 2 // two or more compressed bits per uncompressed nibble
		 || !IsStringAt(content, 0, "S101")
		 || content[4] != 0 || content[5] != 0x3e)
			return false;
		byte[15872] unpacked;
		int unpackedOffset = 0;
		BitStream() bitStream;
		bitStream.Content = content;
		bitStream.ContentOffset = 22;
		bitStream.ContentLength = contentLength;
		int current = bitStream.ReadBits(4);
		int hi = -1;
		for (;;) {
			if (hi < 0)
				hi = current;
			else {
				unpacked[unpackedOffset++] = hi << 4 | current;
				if (unpackedOffset >= 15872)
					break;
				hi = -1;
			}
			int code;
			int bit;
			for (code = 0; ; code += 2) {
				bit = bitStream.ReadBit();
				if (bit == 0)
					break;
				if (bit < 0 || code >= 14)
					return false;
			}
			bit = bitStream.ReadBit();
			if (bit < 0)
				return false;
			code += bit;
			current = current - content[6 + code] & 0xf;
		}
		return DecodeAp3(unpacked, 15872);
	}

	bool DecodeHip(byte[] content, int contentLength)
	{
		if (contentLength < 80)
			return false;
		const byte[] gr10Colors = { 0x00, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e };
		byte[320 * 240] frame1;
		byte[320 * 240] frame2;
		int frameLength = ParseAtari8ExecutableHeader(content, 0);
		if (frameLength > 0
		 && frameLength % 40 == 0
		 && 12 + frameLength * 2 == contentLength
		 && ParseAtari8ExecutableHeader(content, 6 + frameLength) == frameLength) {
			// HIP image with binary file headers.
			int height = frameLength / 40;
			if (height > 240)
				return false;
			SetSize(320, height, RECOILResolution.Xe2x1);
			LeftSkip = 1;
			SetGtiaColors(gr10Colors, 0);
			DecodeAtari8Gr10(content, 6, frame1, 0, 320, height);
			GtiaColors[8] = 0x00;
			DecodeAtari8Gr9(content, 12 + frameLength, 40, frame2, 0, 320, 320, height);
		}
		else {
			// HIP image with GR. 10 palette.
			int height = contentLength / 80;
			if (height > 240)
				return false;
			SetSize(320, height, RECOILResolution.Xe2x1);
			LeftSkip = 1;
			GtiaColors[8] = 0x00;
			DecodeAtari8Gr9(content, 0, 40, frame1, 0, 320, 320, height);
			if (contentLength % 80 == 9)
				SetGtiaColors(content, contentLength - 9);
			else
				SetGtiaColors(gr10Colors, 0);
			DecodeAtari8Gr10(content, height * 40, frame2, 0, 320, height);
		}
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeTip(byte[] content, int contentLength)
	{
		if (contentLength < 129
		 || content[0] != 'T' || content[1] != 'I' || content[2] != 'P'
		 || content[3] != 1 || content[4] != 0)
			return false;
		int width = content[5];
		int height = content[6];
		if (width > 160 || (width & 3) != 0 || height > 119)
			return false;
		int contentStride = width >> 2;
		int frameLength = content[7] | content[8] << 8;
		if (frameLength != contentStride * height
		 || contentLength != 9 + 3 * frameLength)
			return false;

		SetSize(width << 1, height << 1, RECOILResolution.Xe2x2);
		LeftSkip = 1;
		const byte[] colors = { 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x00 };
		SetGtiaColors(colors, 0);
		byte[320 * 238] frame1;
		DecodeAtari8Gr9(content, 9, contentStride, frame1, width << 1, width << 2, width << 1, height);
		DecodeAtari8Gr11PalBlend(content, 9 + 2 * frameLength, contentStride, frame1, 0);
		byte[320 * 238] frame2;
		DecodeAtari8Gr10(content, 9 + frameLength, frame2, width << 1, width << 2, height);
		DecodeAtari8Gr11PalBlend(content, 9 + 2 * frameLength, contentStride, frame2, 0);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeCin(byte[] content, int contentLength)
	{
		int height;
		switch (contentLength) {
		case 15360:
			SetGr15DefaultColors();
			height = 192;
			break;
		case 16004:
			SetBakPF012(content, 16000, 1);
			height = 200;
			break;
		case 16384:
			height = 192;
			break;
		default:
			return false;
		}
		SetSize(320, height, RECOILResolution.Xe2x1);
		byte[320 * 200] frame1;
		byte[320 * 200] frame2;
		for (int y = 0; y < height; y++) {
			if (contentLength == 16384)
				SetBakPF012(content, 0x3c00 + y, 256);
			DecodeAtari8Gr15(content, y * 40, 40, (y & 1) == 0 ? frame1 : frame2, y * 320, 320, 1);
		}
		DecodeAtari8Gr11PalBlend(content, 40 * height + 40, 80, frame1, 1);
		DecodeAtari8Gr11PalBlend(content, 40 * height, 80, frame2, 0);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeCci(byte[] content, int contentLength)
	{
		if (contentLength < 24 || !IsStringAt(content, 0, "CIN 1.2 "))
			return false;
		CciStream() rle;
		rle.Content = content;
		rle.ContentOffset = 8;
		rle.ContentLength = contentLength;
		byte[16384] unpacked;
		if (!rle.UnpackGr15(unpacked, 0))
			return false;
		if (!rle.UnpackGr15(unpacked, 40))
			return false;
		rle.ContentOffset += 4;
		rle.RepeatCount = 0; // DRACONUS.CCI
		if (!rle.Unpack(unpacked, 7680, 40, 7680))
			return false;
		rle.ContentOffset += 4;
		rle.RepeatCount = 0;
		if (!rle.Unpack(unpacked, 15360, 1, 1024))
			return false;
		return DecodeCin(unpacked, 16384);
	}

	bool UnpackRip(byte[] content, int contentOffset, int contentLength, byte[] unpacked, int unpackedLength)
	{
		// "PCK" header (16 bytes)
		if (contentOffset + 304 > contentLength || !IsStringAt(content, contentOffset, "PCK"))
			return false;

		// 288 bytes Shannon-Fano bit lengths
		FanoTree() lengthTree;
		lengthTree.Create(content, contentOffset + 16, 64);
		FanoTree() distanceTree;
		distanceTree.Create(content, contentOffset + 16 + 32, 256);
		FanoTree() literalTree;
		literalTree.Create(content, contentOffset + 16 + 32 + 128, 256);

		// LZ77
		BitStream() bitStream;
		bitStream.Content = content;
		bitStream.ContentOffset = contentOffset + 16 + 288;
		bitStream.ContentLength = contentLength;
		for (int unpackedOffset = 0; unpackedOffset < unpackedLength; ) {
			switch (bitStream.ReadBit()) {
			case -1:
				return false;
			case 0:
				int literal = literalTree.ReadCode(bitStream);
				if (literal < 0)
					return false;
				unpacked[unpackedOffset++] = literal;
				break;
			case 1:
				int distance = distanceTree.ReadCode(bitStream);
				if (distance < 0)
					return false;
				distance += 2;
				if (distance > unpackedOffset)
					return false;
				int length = lengthTree.ReadCode(bitStream);
				if (length < 0)
					return false;
				length += 2;
				do {
					unpacked[unpackedOffset] = unpacked[unpackedOffset - distance];
					if (++unpackedOffset >= unpackedLength)
						return true;
				} while (--length > 0);
				break;
			}
		}
		return true;
	}

	bool DecodeRip(byte[] content, int contentLength)
	{
		if (contentLength < 34
		 || content[0] != 'R' || content[1] != 'I' || content[2] != 'P'
		 || content[18] != 'T' || content[19] != ':')
			return false;
		int headerLength = content[11] | content[12] << 8;
		int contentStride = content[13];
		int height = content[15];
		int textLength = content[17];
		if (headerLength >= contentLength
		 || contentStride == 0 || contentStride > 80 || (contentStride & 1) != 0 || height == 0 || height > 239
		 || 33 + textLength >= contentLength
		 || content[20 + textLength] != 9
		 || !IsStringAt(content, 21 + textLength, "CM:"))
			return false;

		if (content[7] < 0x10)
			contentStride >>= 1;
		int unpackedLength = contentStride * height;
		if (content[7] == 0x30)
			unpackedLength += (height + 1 >> 1) << 3; // Multi RIP: 8 bytes of palette per two lines
		byte[84 * 239] unpacked = 0;
		switch (content[9]) {
		case 0:
			if (headerLength + unpackedLength > contentLength)
				return false;
			content.CopyTo(headerLength, unpacked, 0, unpackedLength);
			break;
		case 1:
			// Ignore errors, because many RIP files included with Atari Interlace Studio are corrupted.
			UnpackRip(content, headerLength, contentLength, unpacked, unpackedLength);
			break;
		default:
			return false;
		}

		SetGtiaColors(content, 24 + textLength);
		contentStride = content[13] >> 1;
		int width = contentStride << 3;
		byte[320 * 239] frame1;
		byte[320 * 239] frame2;
		switch (content[7]) {
		case 0x0e: // GR. 15
			SetSize(width, height, RECOILResolution.Xe2x1);
			DecodeAtari8Gr15(unpacked, 0, contentStride, frame1, 0, width, height);
			return ApplyAtari8Palette(frame1);
		case 0x0f: // GR. 8
			SetSize(width, height, RECOILResolution.Xe1x1);
			DecodeAtari8Gr8(unpacked, 0, frame1, 0, height);
			return ApplyAtari8Palette(frame1);
		case 0x4f: // GR. 9
			SetSize(width, height, RECOILResolution.Xe4x1);
			DecodeAtari8Gr9(unpacked, 0, contentStride, frame1, 0, width, width, height);
			return ApplyAtari8Palette(frame1);
		case 0x8f: // GR. 10
			SetSize(width, height, RECOILResolution.Xe4x1);
			LeftSkip = 2;
			DecodeAtari8Gr10(unpacked, 0, frame1, 0, width, height);
			return ApplyAtari8Palette(frame1);
		case 0xcf: // GR. 11
			SetSize(width, height, RECOILResolution.Xe4x1);
			DecodeAtari8Gr11(content, 0, frame1, 0, width, height);
			return ApplyAtari8Palette(frame1);
		case 0x1e: // GR. 15 blend with one palette
			SetSize(width, height, RECOILResolution.Xe2x1);
			DecodeAtari8Gr15(unpacked, 0, contentStride, frame1, 0, width, height);
			DecodeAtari8Gr15(unpacked, height * contentStride, contentStride, frame2, 0, width, height);
			return ApplyAtari8PaletteBlend(frame1, frame2);
		case 0x10: // GR. 15 blend with different palettes
			SetSize(width, height, RECOILResolution.Xe2x1);
			SetBakPF012(content, 28 + textLength, 1);
			DecodeAtari8Gr15(unpacked, 0, contentStride << 1, frame1, 0, width << 1, height >> 1);
			SetBakPF012(content, 24 + textLength, 1);
			DecodeAtari8Gr15(unpacked, contentStride, contentStride << 1, frame1, width, width << 1, height >> 1);
			DecodeAtari8Gr15(unpacked, height * contentStride, contentStride << 1, frame2, 0, width << 1, height >> 1);
			SetBakPF012(content, 28 + textLength, 1);
			DecodeAtari8Gr15(unpacked, (height + 1) * contentStride, contentStride << 1, frame2, width, width << 1, height >> 1);
			return ApplyAtari8PaletteBlend(frame1, frame2);
		case 0x20: // HIP, RIP
			SetSize(width, height, RECOILResolution.Xe2x1);
			LeftSkip = 1;
			DecodeAtari8Gr10(unpacked, 0, frame1, 0, width, height);
			GtiaColors[8] = 0x00;
			DecodeAtari8Gr9(unpacked, height * contentStride, contentStride, frame2, 0, width, width, height);
			return ApplyAtari8PaletteBlend(frame1, frame2);
		case 0x30: // Multi RIP
			SetSize(width, height, RECOILResolution.Xe2x1);
			LeftSkip = 1;
			GtiaColors[0] = 0x00;
			int colorsOffset = height * contentStride << 1;
			for (int y = 0; y < height; y += 2) {
				SetPM123PF0123Bak(unpacked, colorsOffset + (y << 2));
				DecodeAtari8Gr10(unpacked, y * contentStride, frame1, y * width, width, y + 1 < height ? 2 : 1);
			}
			GtiaColors[8] = 0x00;
			DecodeAtari8Gr9(unpacked, height * contentStride, contentStride, frame2, 0, width, width, height);
			return ApplyAtari8PaletteBlend(frame1, frame2);
		default:
			return false;
		}
	}

	bool DecodeVzi(byte[] content, int contentLength)
	{
		if (contentLength != 16000)
			return false;
		SetSize(320, 200, RECOILResolution.Xe2x1);
		byte[320 * 200] frame1;
		LeftSkip = -1;
		DecodeAtari8Gr9(content, 0, 40, frame1, 0, 320, 320, 200);
		LeftSkip = 1;
		byte[320 * 200] frame2;
		DecodeAtari8Gr9(content, 8000, 40, frame2, 0, 320, 320, 200);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeRm(byte[] content, int contentLength, int mode, RECOILResolution resolution)
	{
		byte[7680] unpacked;
		int colorsOffset;
		int dliOffset;
		if (XeKoalaStream.UnpackWrapped(content, contentLength - 464, unpacked, mode == 0 ? 3840 : 7680)) {
			// File written by a Rambrandt plugin ("DOS module").
			// Documentation suggests RM0-RM4 extensions.
			colorsOffset = contentLength - 464;
			dliOffset = contentLength - 384;
		}
		else if (contentLength == 8192) {
			// Rambrandt native raw format.
			// Rambrandt writes directly to disk using its own filesystem:
			// 10 pictures per disk, 32 characters filename (no extension).
			// We assume RM0-RM4 extensions even though it's a different format.
			content.CopyTo(0, unpacked, 0, 7680);
			colorsOffset = 0x1e00;
			dliOffset = 0x1e80;
		}
		else
			return false;

		bool[192] dliPresent = false;
		for (int i = 0; i < 128; i++) {
			int y = content[dliOffset + i];
			switch (y) {
			case 0:
				break;
			case 1:
			case 2:
			case 4:
			case 5:
				return false;
			default:
				if (mode == 0) {
					if (y >= 5 + 96)
						return false;
					if (y == 3)
						y = 0;
					else
						y -= 5;
				}
				else {
					if (y == 100 || y == 101 || y >= 198)
						return false;
					if (y == 3)
						y = 1;
					else if (y < 100)
						y -= 4;
					else
						y -= 6;
				}
				dliPresent[y] = true;
				break;
			}
		}

		SetSize(320, 192, resolution);
		if (mode == 2)
			LeftSkip = 2;
		if (mode == 1)
			GtiaColors[8] = content[colorsOffset + 8] & 0xf0;
		else
			SetGtiaColors(content, colorsOffset);
		int height = mode == 0 ? 96 : 192;
		byte[320 * 192] frame;
		for (int y = 0; y < height; y++) {
			switch (mode) {
			case 0:
				DecodeAtari8Gr7(unpacked, y * 40, frame, y * 640, 1);
				break;
			case 1:
				DecodeAtari8Gr9(unpacked, y * 40, 40, frame, y * 320, 320, 320, 1);
				break;
			case 2:
				DecodeAtari8Gr10(unpacked, y * 40, frame, y * 320, 320, 1);
				break;
			case 3:
				DecodeAtari8Gr11(unpacked, y * 40, frame, y * 320, 320, 1);
				break;
			case 4:
				DecodeAtari8Gr15(unpacked, y * 40, 40, frame, y * 320, 320, 1);
				break;
			}
			if (dliPresent[y]) {
				int vcount = mode == 0 ? 16 + y : 16 + (y - 1 >> 1);
				int reg = content[dliOffset + 128 + vcount];
				if (reg < 9)
					SetGtiaColor(reg, content[dliOffset + 256 + vcount]);
				else if (reg != 0x80)
					return false;
			}
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeAgp(byte[] content, int contentLength)
	{
		if (contentLength != 7690)
			return false;
		SetGtiaColors(content, 1);
		byte[320 * 192] frame;
		switch (content[0]) {
		case 8:
			SetSize(320, 192, RECOILResolution.Xe1x1);
			DecodeAtari8Gr8(content, 10, frame, 0, 192);
			break;
		case 9:
			SetSize(320, 192, RECOILResolution.Xe4x1);
			DecodeAtari8Gr9(content, 10, 40, frame, 0, 320, 320, 192);
			break;
		case 10:
			SetSize(320, 192, RECOILResolution.Xe4x1);
			LeftSkip = 2;
			DecodeAtari8Gr10(content, 10, frame, 0, 320, 192);
			break;
		case 11:
			SetSize(320, 192, RECOILResolution.Xe4x1);
			DecodeAtari8Gr11(content, 10, frame, 0, 320, 192);
			break;
		case 15:
			SetSize(320, 192, RECOILResolution.Xe2x1);
			DecodeAtari8Gr15(content, 10, 40, frame, 0, 320, 192);
			break;
		default:
			return false;
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeShc(byte[] content, int contentLength)
	{
		if (contentLength != 17920)
			return false;
		SetSize(320, 192, RECOILResolution.Xe1x1);
		byte[320 * 192] frame1;
		byte[320 * 192] frame2;
		int col1 = 0x3c00;
		int col2 = 0x4100;
		for (int y = 0; y < 192; y++) {
			for (int x = 0; x < 320; x++) {
				int i = 320 * y + x;
				int bit = ~x & 7;
				switch (x) {
				// Widths measured in Atari800Win PLus 4.0 and Altirra 2.0-test47.
				case 46 + 48:
				case 46 + 48 + 48+ 24:
				case 46 + 48 + 48+ 24 + 24 + 24:
				case 46 + 48 + 48+ 24 + 24 + 24 + 24 + 24:
				case 46 + 48 + 48+ 24 + 24 + 24 + 24 + 24 + 24 + 20:
					col1++;
					break;
				case 46:
				case 46 + 48 + 48:
				case 46 + 48 + 48 + 24 + 24:
				case 46 + 48 + 48 + 24 + 24 + 24 + 24:
				case 46 + 48 + 48 + 24 + 24 + 24 + 24 + 24 + 24:
					col2++;
					break;
				default:
					break;
				}
				// COLPF1 == 0
				frame1[i] = content[col1] & ((content[i >> 3] >> bit & 1) != 0 ? 0xf0 : 0xfe);
				frame2[i] = content[col2] & ((content[7680 + (i >> 3)] >> bit & 1) != 0 ? 0xf0 : 0xfe);
			}
			col1++;
			col2++;
		}
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeMgp(byte[] content, int contentLength)
	{
		if (contentLength != 3845)
			return false;
		SetSize(320, 192, RECOILResolution.Xe2x2);
		byte[320 * 192] frame;
		SetPF0123Bak(content, 0);
		int rainbow = content[5];

		// The file is missing the last byte of the bitmap.
		byte[3840] bitmap;
		content.CopyTo(6, bitmap, 0, 3839);
		bitmap[3839] = 0; // put background in the missing byte

		for (int y = 0; y < 96; y++) {
			if (rainbow < 4) {
				// Rainbow effect :)
				// The constant 16 is arbitrary here.
				// For correct animation it should decrease every frame.
				GtiaColors[rainbow == 0 ? 8 : 3 + rainbow] = 16 + y & 0xfe;
			}
			DecodeAtari8Gr7(bitmap, y * 40, frame, y * 640, 1);
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeFwa(byte[] content, int contentLength)
	{
		if (contentLength < 0x1f18 || content[0] != 0xfe || content[1] != 0xfe
		 || content[6] != 0x70 || content[7] != 0x70 || content[8] != 0x70 || content[0xb] != 0x50 || content[0x73] != 0x60 || content[0xcd] != 0x41
		 || 0x1f18 + content[0x1f16] + (content[0x1f17] << 8) != contentLength)
			return false;
		SetSize(320, 192, RECOILResolution.Xe2x1);
		byte[320 * 192] frame;
		SetBakPF012(content, 2, 1);
		int dlOffset = 9;
		int dliOffset = 0x1f18;
		for (int y = 0; y < 192; y++) {
			DecodeAtari8Gr15(content, 0x106 + 40 * y + (y >= 102 ? 16 : 0), 40, frame, y * 320, 320, 1);
			int dlInstr = content[dlOffset];
			if (dlOffset == 9 || dlOffset == 0x71) {
				if ((dlInstr & 0x7f) != 0x4e || content[dlOffset + 1] != 0)
					return false;
				dlOffset += 3;
			}
			else {
				if ((dlInstr & 0x7f) != 0x0e)
					return false;
				dlOffset++;
			}
			if (dlInstr >= 0x80) {
				if (dliOffset + 14 > contentLength || content[dliOffset] != 0x48 || content[dliOffset + 1] != 0x8a || content[dliOffset + 2] != 0x48
				 || content[dliOffset + 3] != 0xa9 || content[dliOffset + 5] != 0x8d || content[dliOffset + 6] != 0x0a || content[dliOffset + 7] != 0xd4)
					return false;
				byte a = content[dliOffset + 4];
				dliOffset += 8;
				while (content[dliOffset] != 0x20) {
					switch (content[dliOffset]) {
					case 0xa9:
						a = content[dliOffset + 1];
						dliOffset += 2;
						break;
					case 0x8d:
						if (content[dliOffset + 2] != 0xd0)
							return false;
						int lo = content[dliOffset + 1];
						switch (lo) {
						case 0x16:
						case 0x17:
						case 0x18:
						case 0x1a:
							GtiaColors[lo - 0x12] = a & 0xfe;
							break;
						default:
							return false;
						}
						dliOffset += 3;
						break;
					default:
						return false;
					}
					if (dliOffset + 3 > contentLength)
						return false;
				}
				if (content[dliOffset + 1] != 0xca || content[dliOffset + 2] != 0x06)
					return false;
				dliOffset += 3;
			}
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeAtari8Font(byte[] characters, byte[] font, int fontOffset)
	{
		SetSize(256, 32, RECOILResolution.Xe1x1);
		byte[256 * 32] frame;
		DecodeAtari8Gr0(characters, 32, font, fontOffset, frame);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeAtari8Fnt(byte[] content, int contentLength)
	{
		int contentOffset;
		switch (contentLength) {
		case 1024:
		case 1025:
		case 1026:
			contentOffset = 0;
			break;
		case 1030:
			if (ParseAtari8ExecutableHeader(content, 0) != 1024)
				return false;
			contentOffset = 6;
			break;
		default:
			return false;
		}
		byte[128] characters;
		for (int i = 0; i < 128; i++)
			characters[i] = i;
		return DecodeAtari8Font(characters, content, contentOffset);
	}

	bool DecodeSxs(byte[] content, int contentLength)
	{
		if (contentLength != 1030 || ParseAtari8ExecutableHeader(content, 0) != 1024)
			return false;
		byte[128] characters;
		// 00 01 04 05 08 09 ... 3c 3d
		// 02 03 06 07 0a 0b ... 3e 3f
		// 40 41 44 45 48 49 ... 7c 7d
		// 42 43 46 47 4a 4b ... 7e 7f
		for (int i = 0; i < 128; i++)
			characters[i] = (i & 0x41) | (i >> 4 & 2) | ((i & 0x1e) << 1);
		return DecodeAtari8Font(characters, content, 6);
	}

	bool DecodeNlq(byte[] content, int contentLength)
	{
		if (contentLength < 19 + 90 * 4 || !IsStringAt(content, 0, "DAISY-DOT NLQ FONT") || content[18] != 0x9b)
			return false;
		SetSize(320, 96, RECOILResolution.Xe1x1);
		byte[320 * 96] frame = 0;
		int contentOffset = 19;
		for (int i = 0; i < 91; i++) {
			if (contentOffset >= contentLength)
				return false;
			int width = content[contentOffset];
			if (width == 0 || width > 19)
				return false;
			int nextContentOffset = contentOffset + (width + 1) * 2;
			if (nextContentOffset > contentLength || content[nextContentOffset - 1] != 0x9b)
				return false;
			int c = i < 64 ? i : i < 90 ? i + 1 : 92;
			for (int y = 0; y < 16; y++) {
				for (int x = 0; x < width; x++) {
					int b = content[contentOffset + 1 + (y & 1) * width + x] >> (7 - (y >> 1)) & 1;
					frame[(c & 0xf0 | y) * 320 + (c & 0xf) * 20 + x] = b == 0 ? 0x00 : 0x0e;
				}
			}
			contentOffset = nextContentOffset;
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeAcs(byte[] content, int contentLength)
	{
		if (contentLength != 1028)
			return false;
		byte[16] characters;
		for (int i = 0; i < 16; i++)
			characters[i] = i;
		SetBakPF012(content, 0, 1);
		SetSize(128, 64, RECOILResolution.Xe2x1);
		byte[128 * 64] frame;
		for (int y = 0; y < 8; y++)
			DecodeAtari8Gr12Line(characters, 0, content, 4 + (y << 7), frame, y << 10);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeJgp(byte[] content, int contentLength)
	{
		if (contentLength != 2054 || ParseAtari8ExecutableHeader(content, 0) != 2048)
			return false;
		byte[32] characters;
		for (int i = 0; i < 32; i++)
			characters[i] = i;
		SetGr15DefaultColors();
		SetSize(256, 64, RECOILResolution.Xe2x1);
		byte[256 * 64] frame;
		for (int y = 0; y < 8; y++)
			DecodeAtari8Gr12Line(characters, 0, content, 6 + ((y & 6) << 7) + ((y & 1) << 10), frame, y << 11);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeLeo(byte[] content, int contentLength)
	{
		if (contentLength != 2580)
			return false;
		SetSize(256, 64, RECOILResolution.Xe2x1);
		byte[256 * 64] frame;
		byte[32] characters;
		SetPF0123Bak(content, 0xa00);
		// The last 15 file bytes are RGB colors.
		// We use the configured Atari 8-bit palette instead.
		for (int y = 0; y < 8; y++) {
			for (int x = 0; x < 32; x++)
				characters[x] = content[0x800 + ((x & 1) << 7) + ((y & 1) << 6) + ((y & 6) << 3) + (x >> 1)];
			DecodeAtari8Gr12Line(characters, 0, content, (y & 1) << 10, frame, y << 11);
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeSif(byte[] content, int contentLength)
	{
		if (contentLength != 2048)
			return false;
		SetSize(256, 32, RECOILResolution.Xe2x1);
		GtiaColors[4] = 0x4c;
		GtiaColors[5] = 0xcc;
		GtiaColors[6] = 0x8c;
		GtiaColors[8] = 0x00;
		byte[256 * 32] frame1;
		byte[256 * 32] frame2;
		byte[32] characters;
		for (int i = 0; i < 32; i++)
			characters[i] = i;
		for (int y = 0; y < 4; y++) {
			DecodeAtari8Gr12Line(characters, 0, content, y << 8, frame1, y << 11);
			DecodeAtari8Gr12Line(characters, 0, content, 1024 + (y << 8), frame2, y << 11);
		}
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeDlm(byte[] content, int contentLength)
	{
		if (contentLength != 256)
			return false;
		byte[11 * 16] characters;
		for (int y = 0; y < 16; y++)
			for (int x = 0; x < 11; x++)
				characters[y * 11 + x] = ToAtari8Char(content[y * 16 + 5 + x]);
		SetSize(88, 128, RECOILResolution.Xe1x1);
		byte[88 * 128] frame;
		DecodeAtari8Gr0(characters, 11, BinaryResource("atari8.fnt"), 0, frame);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeSge(byte[] content, int contentLength)
	{
		if (contentLength != 960)
			return false;
		byte[1024] font;
		BinaryResource("atari8.fnt").CopyTo(0, font, 0, 1024);
		for (int i = 0; i < 4; i++) {
			font[0x7d * 8 + 4 + i] = font[0x5b * 8 + i] = 0x0f;
			font[0x7d * 8 + i] = font[0x5b * 8 + 4 + i] = 0xf0;
		}
		SetSize(320, 192, RECOILResolution.Xe1x1);
		byte[320 * 192] frame;
		DecodeAtari8Gr0(content, 40, font, 0, frame);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeAsciiArtEditor(byte[] content, int contentLength)
	{
		if (contentLength <= 0 || content[contentLength - 1] != 0x9b)
			return false;

		const int maxColumns = 64;
		const int maxRows = 24;
		byte[maxColumns * maxRows] characters;
		int columns = 1;
		int x = 0;
		int y = 0;
		for (int contentOffset = 0; contentOffset < contentLength; contentOffset++) {
			int ch = content[contentOffset];
			if (y >= maxRows)
				return false;
			if (ch == 0x9b) {
				if (columns < x)
					columns = x;
				while (x < maxColumns) // pad with spaces
					characters[y * maxColumns + x++] = 0x00;
				x = 0;
				y++;
			}
			else {
				if (x >= maxColumns)
					return false;
				characters[y * maxColumns + x++] = ToAtari8Char(ch);
			}
		}

		SetSize(columns << 3, y << 3, RECOILResolution.Xe1x1);
		byte[maxColumns * 8 * maxRows * 8] frame;
		DecodeAtari8Gr0(characters, maxColumns, BinaryResource("atari8.fnt"), 0, frame);
		return ApplyAtari8Palette(frame);
	}

	bool DecodeMch(byte[] content, int contentLength)
	{
		int width;
		int charactersStride;
		int charactersOffset;
		switch (contentLength) {
		case 30 * 32 * 9 + 5 * 240:
			width = 128;
			charactersStride = 32;
			charactersOffset = 0;
			break;
		case 30 * 40 * 9 + 5 * 240:
			width = 160;
			charactersStride = 40;
			charactersOffset = 0;
			break;
		case 30 * 48 * 9 + 5 * 240:
			width = 176;
			charactersStride = 48;
			charactersOffset = 3;
			break;
		default:
			return false;
		}
		SetSize(width * 2, 240, RECOILResolution.Xe2x1);
		byte[176 * 2 * 240] frame;
		for (int y = 0; y < 240; y++) {
			int colorsOffset = contentLength - 5 * 240 + y;
			for (int x = 0; x < width; x++) {
				int offset = (charactersOffset + (y >> 3) * charactersStride + (x >> 2)) * 9;
				int c = content[offset + 1 + (y & 7)] >> ((~x & 3) * 2) & 3;
				if (c == 3 && content[offset] >= 0x80)
					c = 4;
				offset = (y * width + x) * 2;
				frame[offset + 1] = frame[offset] = content[colorsOffset + c * 240] & 0xfe;
			}
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeAll(byte[] content, int contentLength)
	{
		// "ALL" format of GRAPH.COM by Adam Bienias:
		// 24 bytes - font in line (0-7)
		// n*1024 bytes - fonts
		// 960 bytes - characters
		// 5 bytes - PF0, PF1, PF2, PF3, BAK colors

		if ((contentLength & 0x3ff) != 24 + 960 + 5)
			return false;
		SetPF0123Bak(content, contentLength - 5);
		SetSize(320, 192, RECOILResolution.Xe2x1);
		byte[320 * 192] frame;
		for (int y = 0; y < 24; y++) {
			int fontOffset = 24 + (content[y] << 10);
			if (fontOffset >= contentLength - 965)
				return false;
			DecodeAtari8Gr12Line(content, contentLength - 965 + y * 40, content, fontOffset, frame, y * (8 * 320));
		}
		return ApplyAtari8Palette(frame);
	}

	bool DecodeMcs(byte[] content, int contentLength)
	{
		if (contentLength != 10185)
			return false;
		// "Super Bilder.atr" slideshow:
		// 9 bytes: COLPM0-3, COLPF0-3, COLBAK
		// 8192 bytes: 8 fonts (one font per 3 character lines)
		// 960 bytes: 40x24 characters
		// 640 bytes: missiles and four players
		// 384 bytes: apparently unused
		SetSize(320, 192, RECOILResolution.Xe2x1);
		byte[320 * 192] frame;
		for (int y = 0; y < 192; y++) {
			int fontOffset = 9 + (y / 24 << 10);
			for (int x = 0; x < 320; x++) {
				int ch = content[9 + 8192 + (y >> 3) * 40 + (x >> 3)];
				int c;
				switch (content[fontOffset + ((ch & 0x7f) << 3) + (y & 7)] >> (~x & 6) & 3) {
				case 0:
					// Sprites are 4x width, arranged next to each other in the following order:
					// P0 M0 P1 M1 P2 M2 P3 M3
					c = x / 80; // player/missile number
					int pmgBit = (x >> 3) % 10;
					int pmgOffset;
					if (pmgBit < 8) {
						// player
						pmgBit = 7 - pmgBit;
						pmgOffset = 9 + 8192 + 960 + 128 + 16 + (c << 7);
					}
					else {
						// missile
						pmgBit = (c << 1) | (pmgBit ^ 9);
						pmgOffset = 9 + 8192 + 960 + 16;
					}
					if ((content[pmgOffset + (y >> 1)] >> pmgBit & 1) == 0)
						c = 8; // COLBAK
					break;
				case 1:
					c = 4;
					break;
				case 2:
					c = 5;
					break;
				default: // case 3:
					c = 6 + (ch >> 7);
					break;
				}
				frame[y * 320 + x] = content[c] & 0xfe;
			}
		}
		return ApplyAtari8Palette(frame);
	}

	static int Gr12GtiaNibbleToGr8(int nibble, int ch, bool gtia10)
	{
		switch (nibble) {
		case 0:  // 000 000
		case 1:  // 000 100
		case 4:  // 100 000
		case 5:  // 100 100
			return 0;
		case 2:  // 000 101
		case 6:  // 100 101
			return 1;
		case 3:  // 000 11x
		case 7:  // 100 11x
			return (ch & 0x80) == 0 ? 2 : 3;
		case 8:  // 101 000
			return gtia10 ? 8 : 4;
		case 9:  // 101 100
			return 4;
		case 10: // 101 101
			return 5;
		case 11: // 101 11x
			return (ch & 0x80) == 0 ? 6 : 7;
		case 12: // 11x 000
			return gtia10 || (ch & 0x80) == 0 ? 8 : 12;
		case 13: // 11x 100
			return (ch & 0x80) == 0 ? 8 : 12;
		case 14: // 11x 101
			return (ch & 0x80) == 0 ? 9 : 13;
		case 15: // 11x 11x
			return (ch & 0x80) == 0 ? 10 : 15;
		default:
			// not reachable
			return 0;
		}
	}

	static byte Gr12GtiaByteToGr8(int b, int ch, bool gtia10)
	{
		return Gr12GtiaNibbleToGr8(b >> 4, ch, gtia10) << 4
			| Gr12GtiaNibbleToGr8(b & 0xf, ch, gtia10);
	}

	const int IceFontFrame1 = -1;
	const int IceFontFrame2 = -2;

	void DecodeIceFrame(byte[] content, int charactersOffset, int fontOffset, byte[] frame, IceFrameMode mode)
	{
		int frameOffset = 0;
		byte[40] bitmap;
		for (int y = 0; y < Height; y++) {
			for (int col = 0; col < Width >> 3; col++) {
				int ch;
				switch (charactersOffset) {
				case IceFontFrame1:
					const byte[16] row2char1 = { 0x40, 0x00, 0x20, 0x60, 0xc0, 0x80, 0xa0, 0xe0, 0x40, 0x00, 0x20, 0x60, 0xc0, 0x80, 0xa0, 0xe0 };
					ch = row2char1[y >> 3] + col;
					break;
				case IceFontFrame2:
					const byte[16] row2char2 = { 0x40, 0x00, 0x20, 0x60, 0xc0, 0x80, 0xa0, 0xe0, 0xc0, 0x80, 0xa0, 0xe0, 0x40, 0x00, 0x20, 0x60 };
					ch = row2char2[y >> 3] + col;
					break;
				default: // IRG/IR2/DIN/ICN/IMN/IPC/IP2 picture
					ch = (y / 24 << 8) + content[charactersOffset + (y >> 3) * 40 + col];
					break;
				}
				int b = content[fontOffset + ((ch & ~0x80) << 3) + (y & 7)];
				switch (mode) {
				case IceFrameMode.Gr0:
				case IceFrameMode.Gr0Gtia9:
				case IceFrameMode.Gr0Gtia10:
				case IceFrameMode.Gr0Gtia11:
					if (charactersOffset < 0 && (ch & 0x80) != 0) // GR. 0 inverse only for fonts, it's disabled in CHACTL for pictures
						b ^= 0xff;
					bitmap[col] = b;
					break;
				case IceFrameMode.Gr12:
					for (int x = col == 0 ? LeftSkip : 0; x < 8; x++) {
						int c = b >> (~x & 6) & 3;
						int gr12Registers = (ch & 0x80) == 0 ? 0x6548 : 0x7548;
						frame[frameOffset + (col << 3) + x - LeftSkip] = GtiaColors[gr12Registers >> (c << 2) & 0xf];
					}
					break;
				case IceFrameMode.Gr12Gtia9:
				case IceFrameMode.Gr12Gtia11:
					bitmap[col] = Gr12GtiaByteToGr8(b, ch, false);
					break;
				case IceFrameMode.Gr12Gtia10:
					bitmap[col] = Gr12GtiaByteToGr8(b, ch, true);
					break;
				}
			}
			switch (mode) {
			case IceFrameMode.Gr0:
				DecodeAtari8Gr8(bitmap, 0, frame, frameOffset, 1);
				break;
			case IceFrameMode.Gr12:
				for (int x = Width ; x < Width + LeftSkip; x++)
					frame[frameOffset + x] = GtiaColors[8];
				break;
			case IceFrameMode.Gr0Gtia9:
			case IceFrameMode.Gr12Gtia9:
				DecodeAtari8Gr9(bitmap, 0, 0, frame, frameOffset, 0, Width, 1);
				break;
			case IceFrameMode.Gr0Gtia10:
			case IceFrameMode.Gr12Gtia10:
				DecodeAtari8Gr10(bitmap, 0, frame, frameOffset, 0, 1);
				break;
			case IceFrameMode.Gr0Gtia11:
			case IceFrameMode.Gr12Gtia11:
				DecodeAtari8Gr11(bitmap, 0, frame, frameOffset, 0, 1);
				break;
			}
			frameOffset += Width;
		}
	}

	bool VerifyIce(byte[] content, int contentLength, bool font, int fontLength, int imageLength, RECOILResolution resolution)
	{
		if (font) {
			if (contentLength != fontLength)
				return false;
			SetSize(256, 128, resolution);
		}
		else {
			if (contentLength != imageLength || content[0] != 1)
				return false;
			SetSize(320, 192, resolution);
		}
		return true;
	}

	bool DecodeAtari8Ice(byte[] content, int contentLength, bool font, int mode)
	{
		byte[320 * 192] frame1;
		byte[320 * 192] frame2;
		switch (mode) {
		case 0:
			if (contentLength != 5 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe1x1);
			GtiaColors[5] = content[1] & 0xfe;
			GtiaColors[6] = content[3] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 5, frame1, IceFrameMode.Gr0);
			GtiaColors[5] = content[2] & 0xfe;
			GtiaColors[6] = content[4] & 0xfe;
			DecodeIceFrame(content, IceFontFrame2, 5 + 1024, frame2, IceFrameMode.Gr0);
			break;
		case 1:
			if (!VerifyIce(content, contentLength, font, 6 + 2048, 6 + 16384 + 2 * 960, RECOILResolution.Xe2x1))
				return false;
			SetBakPF0123(content, 1);
			DecodeIceFrame(content, font ? IceFontFrame1 : 6 + 16384, 6, frame1, IceFrameMode.Gr12);
			DecodeIceFrame(content, font ? IceFontFrame2 : 6 + 16384 + 960, 6 + 1024, frame2, IceFrameMode.Gr12);
			break;
		case 2:
			if (!VerifyIce(content, contentLength, font, 10 + 2048, 10 + 16384 + 2 * 960, RECOILResolution.Xe2x1))
				return false;
			GtiaColors[8] = content[1] & 0xfe;
			SetPF0123Even(content, 2);
			DecodeIceFrame(content, font ? IceFontFrame1 : 10 + 16384, 10, frame1, IceFrameMode.Gr12);
			SetPF0123Even(content, 3);
			DecodeIceFrame(content, font ? IceFontFrame2 : 10 + 16384 + 960, 10 + 1024, frame2, IceFrameMode.Gr12);
			break;
		case 3:
			if (font) {
				if (contentLength != 7 + 2048)
					return false;
				SetSize(256, 128, RECOILResolution.Xe1x1);
			}
			else {
				if (contentLength != 7 + 16384 + 960 || content[0] != 3)
					return false;
				SetSize(320, 192, RECOILResolution.Xe1x1);
			}
			SetPF21(content, 1);
			DecodeIceFrame(content, font ? IceFontFrame1 : 7 + 16384, 7, frame1, IceFrameMode.Gr0);
			SetBakPF0123(content, 2);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, font ? IceFontFrame2 : 7 + 16384, 7 + 1024, frame2, IceFrameMode.Gr12);
			break;
		case 4:
			if (contentLength != 10 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe4x1);
			LeftSkip = 2;
			SetGtiaColors(content, 1);
			DecodeIceFrame(content, IceFontFrame1, 10, frame1, IceFrameMode.Gr0Gtia10);
			DecodeIceFrame(content, IceFontFrame2, 10 + 1024, frame2, IceFrameMode.Gr0Gtia10);
			break;
		case 5:
			if (contentLength != 17 + 2048 && contentLength != 18 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe4x1);
			LeftSkip = 2;
			GtiaColors[0] = content[1] & 0xfe;
			for (int i = 0; i < 8; i++)
				SetGtiaColor(i + 1, content[2 + i * 2]);
			if (contentLength == 17 + 2048) {
				DecodeIceFrame(content, IceFontFrame1, 17, frame1, IceFrameMode.Gr0Gtia10);
				for (int i = 0; i < 7; i++)
					SetGtiaColor(i + 1, content[3 + i * 2]);
				DecodeIceFrame(content, IceFontFrame2, 17 + 1024, frame2, IceFrameMode.Gr0Gtia10);
			}
			else {
				DecodeIceFrame(content, IceFontFrame1, 18, frame1, IceFrameMode.Gr0Gtia10);
				for (int i = 0; i < 8; i++)
					SetGtiaColor(i + 1, content[3 + i * 2]);
				DecodeIceFrame(content, IceFontFrame2, 18 + 1024, frame2, IceFrameMode.Gr0Gtia10);
			}
			break;
		case 6:
			if (contentLength != 3 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe4x1);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 3, frame1, IceFrameMode.Gr0Gtia9);
			GtiaColors[8] = content[2] & 0xfe;
			DecodeIceFrame(content, IceFontFrame2, 3 + 1024, frame2, IceFrameMode.Gr0Gtia9);
			break;
		case 7:
			if (contentLength != 3 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe4x1);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 3, frame1, IceFrameMode.Gr0Gtia11);
			GtiaColors[8] = content[2] & 0xfe;
			DecodeIceFrame(content, IceFontFrame2, 3 + 1024, frame2, IceFrameMode.Gr0Gtia11);
			break;
		case 8:
			if (contentLength != 10 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe2x1);
			LeftSkip = 1;
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 10, frame1, IceFrameMode.Gr0Gtia9);
			SetGtiaColors(content, 1);
			DecodeIceFrame(content, IceFontFrame2, 10 + 1024, frame2, IceFrameMode.Gr0Gtia10);
			break;
		case 9:
			if (contentLength != 10 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe2x1);
			LeftSkip = 1;
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 10, frame1, IceFrameMode.Gr0Gtia11);
			GtiaColors[0] = 0x00;
			SetPM123PF0123Bak(content, 2);
			DecodeIceFrame(content, IceFontFrame2, 10 + 1024, frame2, IceFrameMode.Gr0Gtia10);
			break;
		case 10:
			if (contentLength != 3 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe4x1);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 3, frame1, IceFrameMode.Gr0Gtia9);
			GtiaColors[8] = content[2] & 0xfe;
			DecodeIceFrame(content, IceFontFrame2, 3 + 1024, frame2, IceFrameMode.Gr0Gtia11);
			break;
		case 11:
			if (contentLength != 3 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe1x1);
			GtiaColors[6] = 0x00;
			GtiaColors[5] = content[2] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 3, frame1, IceFrameMode.Gr0);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame2, 3 + 1024, frame2, IceFrameMode.Gr0Gtia11);
			break;
		case 12:
			if (contentLength != 3 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe1x1);
			SetPF21(content, 1);
			DecodeIceFrame(content, IceFontFrame1, 3, frame1, IceFrameMode.Gr0);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame2, 3 + 1024, frame2, IceFrameMode.Gr0Gtia9);
			break;
		case 13:
			if (contentLength != 11 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe1x1);
			SetPF21(content, 1);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 11, frame1, IceFrameMode.Gr0);
			// In the original viewer, first frame is scrolled horizontally one pixel to the right.
			// Here, I move GR. 10 to the left.
			// This can't be done on the Atari as it doesn't support scrolling GR. 10 by half a pixel.
			// I apply same technique to ICE modes 16, 19 and IP2.
			LeftSkip = 2;
			GtiaColors[0] = content[1] & 0xfe;
			SetPM123PF0123Bak(content, 3);
			DecodeIceFrame(content, IceFontFrame2, 11 + 1024, frame2, IceFrameMode.Gr0Gtia10);
			break;
		case 14:
			if (contentLength != 6 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe2x1);
			SetBakPF0123(content, 1);
			DecodeIceFrame(content, IceFontFrame2, 6 + 1024, frame2, IceFrameMode.Gr12Gtia11);
			GtiaColors[8] = 0x00;
			DecodeIceFrame(content, IceFontFrame1, 6, frame1, IceFrameMode.Gr12);
			break;
		case 15:
			if (contentLength != 6 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe2x1);
			SetBakPF0123(content, 1);
			DecodeIceFrame(content, IceFontFrame1, 6, frame1, IceFrameMode.Gr12);
			DecodeIceFrame(content, IceFontFrame2, 6 + 1024, frame2, IceFrameMode.Gr12Gtia9);
			break;
		case 16:
			if (contentLength != 10 + 2048)
				return false;
			SetSize(256, 128, RECOILResolution.Xe2x1);
			LeftSkip = 2;
			SetGtiaColors(content, 1);
			DecodeIceFrame(content, IceFontFrame2, 10 + 1024, frame2, IceFrameMode.Gr12Gtia10);
			LeftSkip = 0;
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, IceFontFrame1, 10, frame1, IceFrameMode.Gr12);
			break;
		case 17:
			if (!VerifyIce(content, contentLength, font, 6 + 2048, 6 + 16384 + 960, RECOILResolution.Xe2x1))
				return false;
			SetBakPF0123(content, 1);
			DecodeIceFrame(content, font ? IceFontFrame2 : 6 + 16384, 6 + 1024, frame2, IceFrameMode.Gr0Gtia11);
			GtiaColors[8] = 0x00;
			DecodeIceFrame(content, font ? IceFontFrame1 : 6 + 16384, 6, frame1, IceFrameMode.Gr12);
			break;
		case 18:
			if (!VerifyIce(content, contentLength, font, 6 + 2048, 6 + 16384 + 960, RECOILResolution.Xe2x1))
				return false;
			SetBakPF0123(content, 1);
			DecodeIceFrame(content, font ? IceFontFrame1 : 6 + 16384, 6, frame1, IceFrameMode.Gr12);
			DecodeIceFrame(content, font ? IceFontFrame2 : 6 + 16384, 6 + 1024, frame2, IceFrameMode.Gr0Gtia9);
			break;
		case 19:
			if (!VerifyIce(content, contentLength, font, 10 + 2048, 10 + 16384 + 960, RECOILResolution.Xe2x1))
				return false;
			SetPF0123Bak(content, 5);
			GtiaColors[8] = content[1] & 0xfe;
			DecodeIceFrame(content, font ? IceFontFrame1 : 10 + 16384, 10, frame1, IceFrameMode.Gr12);
			LeftSkip = 2;
			SetGtiaColors(content, 1);
			DecodeIceFrame(content, font ? IceFontFrame2 : 10 + 16384, 10 + 1024, frame2, IceFrameMode.Gr0Gtia10);
			break;
		default:
			return false;
		}
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	bool DecodeIp2(byte[] content, int contentLength)
	{
		if (contentLength != 17358 || content[0] != 1)
			return false;
		SetSize(320, 192, RECOILResolution.Xe2x1);
		GtiaColors[8] = content[1] & 0xfe;
		GtiaColors[4] = content[5] & 0xfe;
		GtiaColors[5] = content[7] & 0xfe;
		GtiaColors[6] = content[9] & 0xfe;
		GtiaColors[7] = content[11] & 0xfe;
		byte[320 * 192] frame1;
		DecodeIceFrame(content, 14 + 16384, 14, frame1, IceFrameMode.Gr12);
		LeftSkip = 2; // see the comment in DecodeAtari8Ice
		for (int i = 0; i < 4; i++) {
			GtiaColors[i] = content[1 + i] & 0xfe;
			SetGtiaColor(4 + i, content[6 + i * 2]);
		}
		SetGtiaColor(8, content[13]);
		byte[320 * 192] frame2;
		DecodeIceFrame(content, 14 + 16384, 14 + 1024, frame2, IceFrameMode.Gr0Gtia10);
		return ApplyAtari8PaletteBlend(frame1, frame2);
	}

	void DecodeAtari8RgbScreen(byte[] screens, int screensOffset, int color, byte[] frame)
	{
		if (Resolution == RECOILResolution.Xe4x1) {
			GtiaColors[8] = color;
			DecodeAtari8Gr9(screens, screensOffset, 40, frame, 0, Width, Width, Height);
		}
		else {
			GtiaColors[8] = 0x00;
			GtiaColors[4] = color | 4;
			GtiaColors[5] = color | 0xa;
			GtiaColors[6] = color | 0xe;
			DecodeAtari8Gr15(screens, screensOffset, 40, frame, 0, Width, Height);
		}
	}

	bool DecodeAtari8Rgb(byte[] content, int contentLength)
	{
		if (contentLength < 9 || !IsStringAt(content, 0, "RGB1"))
			return false;
		int titleLength = content[4];
		int width = content[6 + titleLength];
		int height = content[7 + titleLength];
		if (width == 0 || (width & 1) != 0 || width > 80 || height == 0 || height > 192 || content[8 + titleLength] != 1)
			return false;
		switch (content[5 + titleLength]) {
		case 9:
			SetSize(width << 2, height, RECOILResolution.Xe4x1);
			break;
		case 15:
			SetSize(width << 2, height, RECOILResolution.Xe2x1);
			break;
		default:
			return false;
		}

		int[192] leftRgbs;
		byte[3 * 40 * 192] screens;
		RgbStream() rle;
		rle.Content = content;
		rle.ContentOffset = 9 + titleLength;
		rle.ContentLength = contentLength;
		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				int rgb = rle.ReadRle();
				if (rgb < 0)
					return false;
				if ((x & 1) == 0)
					leftRgbs[y] = rgb;
				else {
					int leftRgb = leftRgbs[y];
					int screenOffset = y * 40 + (x >> 1);
					screens[screenOffset] = (leftRgb >> 4 & 0xf0) | (rgb >> 8);
					screens[40 * 192 + screenOffset] = (leftRgb & 0xf0) | (rgb >> 4 & 0x0f);
					screens[2 * 40 * 192 + screenOffset] = (leftRgb << 4 & 0xf0 | rgb & 0x0f);
				}
			}
		}

		byte[320 * 192] frame1;
		DecodeAtari8RgbScreen(screens, 0, 0x30, frame1);
		byte[320 * 192] frame2;
		DecodeAtari8RgbScreen(screens, 40 * 192, 0xc0, frame2);
		byte[320 * 192] frame3;
		DecodeAtari8RgbScreen(screens, 2 * 40 * 192, 0x70, frame3);
		return ApplyAtari8PaletteBlend3(frame1, frame2, frame3);
	}

	bool DrawBlazingPaddlesVector(byte[] content, int contentLength, byte[] frame, int frameOffset, int index, int startAddress)
	{
		if (index * 2 + 1 >= contentLength)
			return false;
		int contentOffset = content[index * 2] + (content[index * 2 + 1] << 8) - startAddress;
		if (contentOffset < 0)
			return false;
		while (contentOffset < contentLength) {
			int control = content[contentOffset++];
			if (control == 0x08)
				return true;
			// bits 7-4: length-1
			for ( ; control >= 0; control -= 16) {
				// bit 2: pen up */
				if ((control & 4) == 0)
					frame[frameOffset + 1] = frame[frameOffset] = 0x0e;
				// bits 1-0: direction
				switch (control & 3) {
				case 0: //right
					frameOffset += 2;
					break;
				case 1: // left
					frameOffset -= 2;
					break;
				case 2: // up
					frameOffset -= Width;
					break;
				case 3: // down
					frameOffset += Width;
					break;
				}
			}
		}
		return false;
	}

	bool DecodeBlazingPaddlesVectors(byte[] content, int contentLength, int startAddress)
	{
		// The file contains several independent shapes.
		// I layout them in reading order, so that they don't overlap,
		// the baselines are aligned and everything fits in 160x240.
		int x = 0;
		int y = 0;
		int i;
		int lineI = 0;
		int lineTop = 0;
		int lineBottom = 0;
		int[256] xs;
		int[256] ys;

		int width = 0;
		BlazingPaddlesBoundingBox() box;
		for (i = 0; i < 256; i++) {
			if (!box.Calculate(content, contentLength, i, startAddress))
				break;
			int shapeWidth = box.Right - box.Left + 2; // +1 because box.right is inclusive, +1 for space
			if (x + shapeWidth > 160) {
				// new line
				y -= lineTop;
				while (lineI < i)
					ys[lineI++] = y;
				if (width < x)
					width = (x + 3) & ~3; // round up to 4 pixels
				x = 0;
				y += lineBottom + 2; // +1 because box.bottom is inclusive, +1 for space
				lineTop = box.Top;
				lineBottom = box.Bottom;
			}
			// place this shape at x,y
			xs[i] = x - box.Left;
			x += shapeWidth;
			if (lineTop > box.Top)
				lineTop = box.Top;
			if (lineBottom < box.Bottom)
				lineBottom = box.Bottom;
		}
		y -= lineTop;
		while (lineI < i)
			ys[lineI++] = y;
		if (width < x)
			width = (x + 3) & ~3; // round up to 4 pixels
		y += lineBottom + 1; // +1 because box.bottom is inclusive
		if (i == 0 || y > 240)
			return false;

		// draw shapes
		SetSize(width * 2, y, RECOILResolution.Xe2x1);
		byte[320 * 240] frame = 0;
		for (i = 0; i < 256; i++) {
			if (!DrawBlazingPaddlesVector(content, contentLength, frame, (ys[i] * width + xs[i]) * 2, i, startAddress))
				break;
		}

		return ApplyAtari8Palette(frame);
	}

	bool DecodeChr(byte[] content, int contentLength)
		return contentLength == 3072 && DecodeBlazingPaddlesVectors(content, contentLength, 0x7000);

	bool DecodeShp(byte[] content, int contentLength)
	{
		switch (contentLength) {
		case 1024:
			return DecodeBlazingPaddlesVectors(content, contentLength, 0x7c00);
		case 4384:
			return DecodeGr7(content, 0x210, 3844);
		default:
			return false;
		}
	}

	static void DrawSpcChar(byte[] pixels, int x1, int y1, int ch)
	{
		// The original program can print garbage from ROM.
		// We don't have the ROM, so just skip these characters.
		// FIXME: check lowercase and semigraphic.
		if (ch < 0x20 || ch > 0x5f)
			return;

		byte[] font = BinaryResource("atari8.fnt");
		int fontOffset = (ch - 0x20) << 3;
		for (int y = 0; y < 8 && y1 + y < 192; y++) {
			for (int x = 0; x < 4 && x1 + x < 160; x++)
				pixels[(y1 + y) * 160 + x1 + x] = font[fontOffset + y] >> (6 - x * 2) & 3;
		}
	}

	static void DrawSpcLine(byte[] pixels, int x1, int y1, int x2, int y2, int color)
	{
		int dx = x2 - x1;
		int dy = y2 - y1;
		// FIXME: I do standard Bresenham's algorithm here,
		// but the original program does something strange involving division of dx/dy.

		// The original draws right or down, so I try it too.
		if (dx < 0)
			dx = -dx;
		if (dy < 0)
			dy = -dy;
		if (dx >= dy) {
			int e = dx;
			if (x2 < x1) {
				// swap points to draw right
				int ty = y1;
				x1 = x2;
				x2 += dx;
				y1 = y2;
				y2 = ty;
			}
			for ( ; x1 <= x2; x1++) {
				if (x1 < 160 && y1 < 192)
					pixels[160 * y1 + x1] = color;
				e -= dy * 2;
				if (e < 0) {
					e += dx * 2;
					y1 += y1 < y2 ? 1 : -1;
				}
			}
		}
		else {
			int e = dy;
			if (y2 < y1) {
				// swap points to draw down
				int tx = x1;
				x1 = x2;
				x2 = tx;
				y1 = y2;
				y2 += dy;
			}
			for ( ; y1 <= y2; y1++) {
				if (x1 < 160 && y1 < 192)
					pixels[160 * y1 + x1] = color;
				e -= dx * 2;
				if (e < 0) {
					e += dy * 2;
					x1 += x1 < x2 ? 1 : -1;
				}
			}
		}
	}

	static void PlotSpcPattern(byte[] pixels, int x, int y, int pattern)
	{
		pixels[y * 160 + x] = pattern >> ((~y & 1) * 8 + (~x & 3) * 2) & 3;
	}

	static void DrawSpcBrush(byte[] pixels, int x1, int y1, int brush, int pattern)
	{
		const byte[8 * 16] brushes = {
			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
			0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
			0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x38, 0x38, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
			0x00, 0x00, 0x10, 0x38, 0x38, 0x7c, 0x7c, 0x7c, 0x7c, 0x38, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00,
			0x00, 0x18, 0x18, 0x3c, 0x3c, 0x7e, 0x7e, 0x7e, 0x7e, 0x3c, 0x3c, 0x18, 0x18, 0x00, 0x00, 0x00,
			0x10, 0x38, 0x7c, 0x7c, 0x7c, 0xfe, 0xfe, 0xfe, 0xfe, 0x7c, 0x7c, 0x7c, 0x38, 0x10, 0x00, 0x00,
			0x00, 0x00, 0x10, 0x28, 0x28, 0x50, 0x3c, 0x78, 0x14, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00,
			0x10, 0x28, 0x54, 0x28, 0x54, 0xba, 0x7c, 0x7c, 0xba, 0x54, 0x28, 0x54, 0x28, 0x10, 0x00, 0x00
		};
		for (int y = 0; y < 16 && y1 + y < 192; y++) {
			int brushShape = brushes[brush * 16 + y];
			for (int x = 0; x < 8 && x1 + x < 160; x++) {
				if ((brushShape >> (7 - x) & 1) != 0)
					PlotSpcPattern(pixels, x, y, pattern);
			}
		}
	}

	static bool FillSpc(byte[] pixels, int x, int y, int pattern)
	{
		if (x >= 160 || y >= 192)
			return false;
		// This is NOT real flood fill.
		while (y >= 0 && pixels[y * 160 + x] == 0)
			y--;
		while (++y < 192 && pixels[y * 160 + x] == 0) {
			do
				x--;
			while (x >= 0 && pixels[y * 160 + x] == 0);
			int x1 = x;
			while (x < 159 && pixels[y * 160 + ++x] == 0)
				PlotSpcPattern(pixels, x, y, pattern);
#if true
			// the original program seems to do this
			// FIXME: probably different if borders reached
			x = x1 + ((x - x1 + 1) >> 1);
#else
			// simpler, but different from above I guess
			x -= (x - x1) >> 1;
#endif
		}
		return true;
	}

	bool DecodeAtari8Spc(byte[] content, int contentLength)
	{
		if (contentLength < 3
		 || contentLength != content[0] + (content[1] << 8) + 3
		 || content[contentLength - 1] != 0)
			return false;

		byte[160 * 192] pixels = 0;
		int[96] lineColors = 0;
		int textX = 0;
		int textY = 0;
		int lineX = 0;
		int lineY = 0;
		int brush = 0;
		int pattern = 0x2288;
		int lineColor = 3;
		int x;
		int y;
		for (int contentOffset = 2; content[contentOffset] != 0; ) {
			switch (content[contentOffset]) {
			case 0x10:
				if (contentOffset + 3 >= contentLength)
					return false;
				textX = content[contentOffset + 1];
				textY = content[contentOffset + 2];
				contentOffset += 3;
				break;
			case 0x20:
			case 0x21:
			case 0x22:
			case 0x23:
				if (contentOffset + 1 >= contentLength)
					return false;
				lineColor = content[contentOffset] & 3;
				contentOffset++;
				break;
			case 0x30:
			case 0x50:
				if (contentOffset + 2 >= contentLength)
					return false;
				DrawSpcChar(pixels, textX, textY, content[contentOffset + 1]);
				textX += 4;
				contentOffset += 2;
				break;
			case 0x40:
			case 0x41:
			case 0x42:
			case 0x43:
			case 0x44:
			case 0x45:
			case 0x46:
			case 0x47:
				if (contentOffset + 1 >= contentLength)
					return false;
				brush = content[contentOffset] & 7;
				contentOffset++;
				break;
			case 0x60:
				if (contentOffset + 2 >= contentLength)
					return false;
				pattern = content[contentOffset + 1];
				const int[] patterns = { // ushort?
					0x0000, 0x5555, 0xaaaa, 0xffff, 0x1144, 0x2288, 0x33cc, 0x6699,
					0x77dd, 0xbbee, 0x1551, 0x2aa2, 0x3ff3, 0x4004, 0x6aa6, 0x7ff7,
					0x8008, 0x9559, 0xbffb, 0xc00c, 0xd55d, 0xeaae, 0x5588, 0x22cc,
					0x3344, 0x4499, 0x44dd, 0x7799, 0x88ee, 0x8866, 0x99ee, 0xcc77,
					0xccbb, 0xddbb, 0x1bb1, 0x1559, 0x155d, 0x955d, 0x4008, 0x400c,
					0x800c, 0x6aac, 0x2aae, 0x6aa2, 0x7ffb, 0x3ffb, 0xbff7, 0x5584,
					0x22c4, 0x3348, 0x4491, 0x44d1, 0x7791, 0x88e2, 0x8862, 0x99e1,
					0xcc73, 0xccb3, 0xddb3, 0x558c, 0x22c8, 0x334c, 0x449d, 0x44d9,
					0x779d, 0x88e6, 0x886e, 0x99e5, 0xcc7b, 0xccb7, 0xddb7
				};
				if (pattern >= patterns.Length)
					return false;
				pattern = patterns[pattern];
				contentOffset += 2;
				break;
			case 0x70:
				if (contentOffset + 7 >= contentLength)
					return false;
				for (y = content[contentOffset + 1]; y <= content[contentOffset + 2]; y++) {
					if (y >= 96)
						return false;
					lineColors[y] = contentOffset + 3;
				}
				contentOffset += 7;
				break;
			case 0x80:
				if (contentOffset + 3 >= contentLength)
					return false;
				lineX = content[contentOffset + 1];
				lineY = content[contentOffset + 2];
				contentOffset += 3;
				break;
			case 0xa0:
				if (contentOffset + 3 >= contentLength)
					return false;
				x = content[contentOffset + 1];
				y = content[contentOffset + 2];
				DrawSpcLine(pixels, lineX, lineY, x, y, lineColor);
				lineX = x;
				lineY = y;
				contentOffset += 3;
				break;
			case 0xc0:
				if (contentOffset + 3 >= contentLength)
					return false;
				DrawSpcBrush(pixels, content[contentOffset + 1], content[contentOffset + 2], brush, pattern);
				contentOffset += 3;
				break;
			case 0xe0:
				if (contentOffset + 3 >= contentLength)
					return false;
				if (!FillSpc(pixels, content[contentOffset + 1], content[contentOffset + 2], pattern))
					return false;
				contentOffset += 3;
				break;
			default:
				return false;
			}
		}

		SetSize(320, 192, RECOILResolution.Xe2x1);
		byte[320 * 192] frame;
		for (y = 0; y < 192; y++) {
			const byte[4] defaultColors = { 0x00, 0x15, 0x95, 0x36 };
			int colorsOffset = lineColors[y >> 1];
			byte[] colors = colorsOffset == 0 ? defaultColors : content;
			for (x = 0; x < 160; x++) {
				int offset = y * 320 + x * 2;
				frame[offset + 1] = frame[offset] = colors[colorsOffset + pixels[y * 160 + x]] & 0xfe;
			}
		}
		return ApplyAtari8Palette(frame);
	}

	// PC formats.

	bool DecodeMsp(byte[] content, int contentLength)
	{
		if (contentLength < 32)
			return false;
		int width = content[4] | content[5] << 8;
		int height = content[6] | content[7] << 8;
		width = width + 7 & ~7; // round up to 8 pixels; FIXME
		int bitmapLength = (width >> 3) * height;
		if (IsStringAt(content, 0, "DanM")) // Microsoft Paint 1
			return contentLength == 32 + bitmapLength && DecodeBlackAndWhite(content, 32, width, height, 0, RECOILResolution.Pc1x1);
		if (IsStringAt(content, 0, "LinS")) {
			// Microsoft Paint 2
			MspStream() rle;
			rle.Content = content;
			rle.ContentOffset = 32 + (height << 1);
			rle.ContentLength = contentLength;
			byte[] unpacked = new byte[bitmapLength];
			bool ok = rle.Unpack(unpacked, 0, 1, bitmapLength) && DecodeBlackAndWhite(unpacked, 0, width, height, 0, RECOILResolution.Pc1x1);
			delete unpacked;
			return ok;
		}
		return false;
	}

	bool DecodeAwbmPalette(byte[] content, int contentLength, int paletteOffset, int colors)
	{
		if (!IsStringAt(content, paletteOffset, "RGB ") || contentLength < paletteOffset + 4 + colors * 3)
			return false;
		for (int i = 0; i < colors; i++) {
			int r = content[paletteOffset + 4 + i * 3];
			int g = content[paletteOffset + 5 + i * 3];
			int b = content[paletteOffset + 6 + i * 3];
			int rgb = r << 16 | g << 8 | b;
			ContentPalette[i] = (rgb & 0x3f3f3f) << 2 | (rgb >> 4 & 0x030303);
		}
		return true;
	}

	bool DecodeAwbm(byte[] content, int contentLength)
	{
		int width = content[4] | content[5] << 8;
		int height = content[6] | content[7] << 8;
		int planeStride = width + 7 >> 3;
		bool colors256;
		if (DecodeAwbmPalette(content, contentLength, 8 + width * height, 256))
			colors256 = true;
		else if ((width & 3) == 0 && DecodeAwbmPalette(content, contentLength, 8 + (height * planeStride << 2), 16))
			colors256 = false;
		else
			return false;
		if (!SetSize(width, height, RECOILResolution.Pc1x1))
			return false;
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				int c;
				if (colors256)
					c = content[8 + y * width + x];
				else {
					int offset = 8 + (y * planeStride << 2) + (x >> 3);
					c = 0;
					for (int bit = 0; bit < 4; bit++) {
						c |= (content[offset] >> (~x & 7) & 1) << bit;
						offset += planeStride;
					}
				}
				Pixels[y * width + x] = ContentPalette[c];
			}
		}
		return true;
	}

	bool DecodeEpa(byte[] content, int contentLength)
	{
		if (contentLength < 2 + 15)
			return false;
		if (IsStringAt(content, 0, "AWBM"))
			return DecodeAwbm(content, contentLength);
		int columns = content[0];
		int rows = content[1];
		if (columns > 80 || rows > 25 || contentLength != 2 + columns * rows * 15 + 70)
			return false;
		int width = columns * 8;
		int height = rows * 14;
		SetSize(width, height, RECOILResolution.Pc1x1);
		int bitmapOffset = 2 + columns * rows;
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				int ch = y / 14 * columns + (x >> 3);
				int attribute = content[2 + ch];
				int b = content[bitmapOffset + ch * 14 + y % 14] >> (~x & 7) & 1;
				const int[16] palette = {
					0x000000,
					0x0000aa,
					0x00aa00,
					0x00aaaa,
					0xaa0000,
					0xaa00aa,
					0xaa5500,
					0xaaaaaa,
					0x555555,
					0x5555ff,
					0x55ff55,
					0x55ffff,
					0xff5555,
					0xff55ff,
					0xffff55,
					0xffffff
				};
				Pixels[y * width + x] = palette[b == 0 ? attribute >> 4 : attribute & 0xf];
			}
		}
		return true;
	}

	static int PackExt(string ext)
		return ext.Length == 0 || ext.Length > 4 ? 0
			: ext[0] + (ext.Length >= 2 ? ext[1] << 8 : 0) + (ext.Length >= 3 ? ext[2] << 16 : 0) + (ext.Length >= 4 ? ext[3] << 24 : 0) | 0x20202020;

	static int GetPackedExt(string filename)
	{
		int ext = 0;
		for (int i = filename.Length; --i >= 0; ) {
			int c = filename[i];
			if (c == '.')
				return ext | 0x20202020;
			if (c <= ' ' || c > 'z' || ext >= 1 << 24)
				return 0;
			ext = (ext << 8) + c;
		}
		return 0;
	}

	/// Checks whether the filename extension is supported by RECOIL.
	/// `true` doesn't necessarily mean that the file contents is valid for RECOIL.
	/// This function is meant to avoid reading files which are known to be
	/// not supported (another criterium is the maximum file size, `MaxContentLength`).
	public static bool IsOurFile(string filename)
	{
		switch (GetPackedExt(filename)) {
		case PackExt("256"):
		case PackExt("3"):
		case PackExt("4BT"):
		case PackExt("4MI"):
		case PackExt("4PL"):
		case PackExt("4PM"):
		case PackExt("64C"):
		case PackExt("A4R"):
		case PackExt("A64"):
		case PackExt("WIG"):
		case PackExt("AAS"):
		case PackExt("IPH"):
		case PackExt("ACS"):
		case PackExt("AFL"):
		case PackExt("AGP"):
		case PackExt("ALL"):
		case PackExt("AMI"):
		case PackExt("AP2"):
		case PackExt("AP3"):
		case PackExt("APV"):
		case PackExt("DGI"):
		case PackExt("DGP"):
		case PackExt("ESC"):
		case PackExt("ILC"):
		case PackExt("PZM"):
		case PackExt("APC"):
		case PackExt("APL"):
		case PackExt("APP"):
		case PackExt("ART"):
		case PackExt("ATR"):
		case PackExt("BB0"):
		case PackExt("BB1"):
		case PackExt("BB2"):
		case PackExt("BB4"):
		case PackExt("BB5"):
		case PackExt("BBG"):
		case PackExt("BFLI"):
		case PackExt("BG9"):
		case PackExt("G09"):
		case PackExt("BGP"):
		case PackExt("BKG"):
		case PackExt("BML"):
		case PackExt("BRU"):
		case PackExt("CA1"):
		case PackExt("CA2"):
		case PackExt("CA3"):
		case PackExt("CCI"):
		case PackExt("CDU"):
		case PackExt("CH4"):
		case PackExt("CH6"):
		case PackExt("CH8"):
		case PackExt("CHE"):
		case PackExt("CHR"):
		case PackExt("CIN"):
		case PackExt("CPR"):
		case PackExt("CPT"):
		case PackExt("CWG"):
		case PackExt("DC1"):
		case PackExt("DD"):
		case PackExt("DEL"):
		case PackExt("DG1"):
		case PackExt("DGC"):
		case PackExt("DGU"):
		case PackExt("DHR"):
		case PackExt("DIN"):
		case PackExt("DLM"):
		case PackExt("DOL"):
		case PackExt("VID"):
		case PackExt("DOO"):
		case PackExt("DPH"):
		case PackExt("DRG"):
		case PackExt("DRL"):
		case PackExt("DLP"):
		case PackExt("DRZ"):
		case PackExt("DRP"):
		case PackExt("DU1"):
		case PackExt("DUO"):
		case PackExt("DU2"):
		case PackExt("ECI"):
		case PackExt("ECP"):
		case PackExt("EPA"):
		case PackExt("FD2"):
		case PackExt("FFLI"):
		case PackExt("FLI"):
		case PackExt("FNT"):
		case PackExt("FPT"):
		case PackExt("FTC"):
		case PackExt("FUN"):
		case PackExt("FP2"):
		case PackExt("FWA"):
		case PackExt("GG"):
		case PackExt("GHG"):
		case PackExt("GOD"):
		case PackExt("G10"):
		case PackExt("G11"):
		case PackExt("GFB"):
		case PackExt("GR7"):
		case PackExt("GR8"):
		case PackExt("GR9"):
		case PackExt("GUN"):
		case PackExt("HED"):
		case PackExt("HFC"):
		case PackExt("HGB"):
		case PackExt("HGR"):
		case PackExt("HIM"):
		case PackExt("HIP"):
		case PackExt("HIR"):
		case PackExt("HBM"):
		case PackExt("GIH"):
		case PackExt("FGS"):
		case PackExt("MON"):
		case PackExt("GCD"):
		case PackExt("HLF"):
		case PackExt("HLR"):
		case PackExt("HPC"):
		case PackExt("HPM"):
		case PackExt("HR"):
		case PackExt("HR2"):
		case PackExt("HCI"):
		case PackExt("ICE"):
		case PackExt("ICN"):
		case PackExt("IFF"):
		case PackExt("ACBM"):
		case PackExt("BL1"):
		case PackExt("BL2"):
		case PackExt("BL3"):
		case PackExt("HAM6"):
		case PackExt("HAM8"):
		case PackExt("LBM"):
		case PackExt("IFL"):
		case PackExt("IGE"):
		case PackExt("IHE"):
		case PackExt("IMG"):
		case PackExt("XIMG"):
		case PackExt("IMN"):
		case PackExt("INFO"):
		case PackExt("ING"):
		case PackExt("INP"):
		case PackExt("INT"):
		case PackExt("IP2"):
		case PackExt("IPC"):
		case PackExt("IPT"):
		case PackExt("IR2"):
		case PackExt("IRG"):
		case PackExt("ISM"):
		case PackExt("IST"):
		case PackExt("JGP"):
		case PackExt("JJ"):
		case PackExt("KOA"):
		case PackExt("GIG"):
		case PackExt("RPM"):
		case PackExt("LEO"):
		case PackExt("LP3"):
		case PackExt("LUM"):
		case PackExt("MAC"):
		case PackExt("MAX"):
		case PackExt("MBG"):
		case PackExt("MC"):
		case PackExt("MCH"):
		case PackExt("MCI"):
		case PackExt("MCP"):
		case PackExt("MCPP"):
		case PackExt("MCS"):
		case PackExt("MG1"):
		case PackExt("MG2"):
		case PackExt("MG4"):
		case PackExt("MG8"):
		case PackExt("MGP"):
		case PackExt("MIC"):
		case PackExt("MIL"):
		case PackExt("MIS"):
		case PackExt("MLE"):
		case PackExt("MPP"):
		case PackExt("MSP"):
		case PackExt("NEO"):
		case PackExt("NLQ"):
		case PackExt("OCP"):
		case PackExt("P"):
		case PackExt("P4I"):
		case PackExt("P64"):
		case PackExt("PAC"):
		case PackExt("PC1"):
		case PackExt("PC2"):
		case PackExt("PC3"):
		case PackExt("PCS"):
		case PackExt("PGC"):
		case PackExt("PGF"):
		case PackExt("PI"):
		case PackExt("PI1"):
		case PackExt("PI2"):
		case PackExt("PI3"):
		case PackExt("PI4"):
		case PackExt("PI9"):
		case PackExt("PIC"):
		case PackExt("PL4"):
		case PackExt("PLA"):
		case PackExt("PLM"):
		case PackExt("PMD"):
		case PackExt("PMG"):
		case PackExt("RAW"):
		case PackExt("RGB"):
		case PackExt("RIP"):
		case PackExt("RM0"):
		case PackExt("RM1"):
		case PackExt("RM2"):
		case PackExt("RM3"):
		case PackExt("RM4"):
		case PackExt("RP"):
		case PackExt("RYS"):
		case PackExt("SAR"):
		case PackExt("SC2"):
		case PackExt("GRP"):
		case PackExt("SC8"):
		case PackExt("SCA"):
		case PackExt("SCC"):
		case PackExt("SRS"):
		case PackExt("SCR"):
		case PackExt("SCS4"):
		case PackExt("SGE"):
		case PackExt("SHC"):
		case PackExt("SHP"):
		case PackExt("SHR"):
		case PackExt("SIF"):
		case PackExt("SPC"):
		case PackExt("SPS"):
		case PackExt("SPU"):
		case PackExt("SR8"):
		case PackExt("SXS"):
		case PackExt("TIP"):
		case PackExt("TN1"):
		case PackExt("TN2"):
		case PackExt("TN3"):
		case PackExt("TNY"):
		case PackExt("TRP"):
		case PackExt("TRU"):
		case PackExt("VIC"):
		case PackExt("VZI"):
		case PackExt("WND"):
		case PackExt("XLP"):
		case PackExt("ZXP"):
			return true;
		default:
			return false;
		}
	}

	/// Decodes Atari picture file to an RGB bitmap.
	/// Returns `true` on success.
	public bool Decode(
		/// Name of the file to decode. Only the extension is processed, for format recognition.
		string filename,
		/// File contents.
		byte[] content,
		/// File length.
		int contentLength)
	{
		switch (GetPackedExt(filename)) {
		case PackExt("256"):
			return Decode256(content, contentLength);
		case PackExt("3"):
			return Decode3(content, contentLength);
		case PackExt("4BT"):
			return Decode4bt(content, contentLength);
		case PackExt("4MI"):
			return Decode4mi(content, contentLength);
		case PackExt("4PL"):
			return Decode4pl(content, contentLength);
		case PackExt("4PM"):
			return Decode4pm(content, contentLength);
		case PackExt("64C"):
			return Decode64c(content, contentLength);
		case PackExt("A4R"):
			return DecodeA4r(content, contentLength);
		case PackExt("A64"):
		case PackExt("WIG"):
			return contentLength == 10242 && DecodeC64Multicolor(320, content, 2, 0x2002, 0x2402, content[0x2801]);
		case PackExt("AAS"):
		case PackExt("IPH"):
			return DecodeIph(content, contentLength);
		case PackExt("ACS"):
			return DecodeAcs(content, contentLength);
		case PackExt("AFL"):
			return DecodeAfl(content, contentLength);
		case PackExt("AGP"):
			return DecodeAgp(content, contentLength);
		case PackExt("ALL"):
			return DecodeAll(content, contentLength);
		case PackExt("AMI"):
			return DecodeAmi(content, contentLength);
		case PackExt("AP2"):
			return Decode256(content, contentLength);
		case PackExt("AP3"):
		case PackExt("APV"):
		case PackExt("DGI"):
		case PackExt("DGP"):
		case PackExt("ESC"):
		case PackExt("ILC"):
		case PackExt("PZM"):
			return DecodeAp3(content, contentLength);
		case PackExt("APC"):
			return DecodeApc(content, contentLength);
		case PackExt("APL"):
			return DecodeApl(content, contentLength);
		case PackExt("APP"):
			return DecodeApp(content, contentLength);
		case PackExt("ART"):
			return DecodeIph(content, contentLength)
				|| DecodeArtDirector(content, contentLength)
				|| DecodeAtari8Artist(content, contentLength)
				|| DecodeAsciiArtEditor(content, contentLength);
		case PackExt("ATR"):
			return DecodeAtr(content, contentLength);
		case PackExt("BB0"):
			return DecodeBb0(content, contentLength, BbcPalette1Bit);
		case PackExt("BB1"):
			return DecodeBb1(content, contentLength, BbcPalette2Bit);
		case PackExt("BB2"):
			return DecodeBb2(content, contentLength, BbcPalette);
		case PackExt("BB4"):
			return DecodeBb4(content, contentLength, BbcPalette1Bit);
		case PackExt("BB5"):
			return DecodeBb5(content, contentLength, BbcPalette2Bit);
		case PackExt("BBG"):
			return DecodeBbg(content, contentLength);
		case PackExt("BFLI"):
			return DecodeBfli(content, contentLength);
		case PackExt("BG9"):
		case PackExt("G09"):
			return DecodeG09(content, contentLength);
		case PackExt("BGP"):
			return DecodeBgp(content, contentLength);
		case PackExt("BKG"):
			return DecodeBkg(content, contentLength);
		case PackExt("BML"):
			return DecodeBml(content, contentLength);
		case PackExt("BRU"):
			return DecodeBru(content, contentLength);
		case PackExt("CA1"):
		case PackExt("CA2"):
		case PackExt("CA3"):
			return DecodeCa(content, contentLength);
		case PackExt("CCI"):
			return DecodeCci(content, contentLength);
		case PackExt("CDU"):
			return contentLength == 10277 && DecodeC64Multicolor(320, content, 0x113, 0x2053, 0x243b, content[0x2823]);
		case PackExt("CH4"):
		case PackExt("CH6"):
		case PackExt("CH8"):
			return DecodeCh8(content, contentLength);
		case PackExt("CHE"):
			return contentLength == 20482 && DecodeC64Multicolor(320, content, 2, 0x4202, 0x4802, 0);
		case PackExt("CHR"):
			return DecodeChr(content, contentLength);
		case PackExt("CIN"):
			return DecodeCin(content, contentLength);
		case PackExt("CPR"):
			return DecodeCpr(content, contentLength);
		case PackExt("CPT"):
			return DecodeCpt(content, contentLength);
		case PackExt("CWG"):
			return contentLength == 10007 && DecodeC64Multicolor(320, content, 2, 0x1f42, 0x232a, content[0x2713]);
		case PackExt("DC1"):
		case PackExt("DGC"):
			return DecodeDgc(content, contentLength);
		case PackExt("DD"):
			return DecodeDd(content, contentLength);
		case PackExt("DEL"):
			return DecodeDel(content, contentLength);
		case PackExt("DG1"):
		case PackExt("DGU"):
			return DecodeDgu(content, contentLength);
		case PackExt("DHR"):
			return DecodeDhr(content, contentLength);
		case PackExt("DIN"):
			return DecodeAtari8Ice(content, contentLength, false, 3);
		case PackExt("DLM"):
			return DecodeDlm(content, contentLength);
		case PackExt("DOL"):
		case PackExt("VID"):
			return DecodeDol(content, contentLength);
		case PackExt("DOO"):
			return DecodeDoo(content, contentLength);
		case PackExt("DPH"):
			return DecodeDph(content, contentLength);
		case PackExt("DRG"):
			return DecodeDrg(content, contentLength);
		case PackExt("DRL"):
		case PackExt("DLP"):
			return DecodeDrl(content, contentLength);
		case PackExt("DRZ"):
		case PackExt("DRP"):
			return DecodeDrz(content, contentLength);
		case PackExt("DU1"):
		case PackExt("DUO"):
			return DecodeDuo(content, contentLength);
		case PackExt("DU2"):
			return DecodeDu2(content, contentLength);
		case PackExt("ECI"):
			return DecodeEci(content, contentLength);
		case PackExt("ECP"):
			return DecodeEcp(content, contentLength);
		case PackExt("EPA"):
			return DecodeEpa(content, contentLength);
		case PackExt("FD2"):
			return DecodeFli(content, contentLength);
		case PackExt("FFLI"):
			return DecodeFfli(content, contentLength);
		case PackExt("FLI"):
			return DecodeFli(content, contentLength)
				|| DecodeBml(content, contentLength);
		case PackExt("FNT"):
			return DecodeAtari8Fnt(content, contentLength)
				|| DecodeAmstradFnt(content, contentLength);
		case PackExt("FPT"):
			return contentLength == 10004 && DecodeC64Multicolor(320, content, 2, 0x1f42, 0x232a, content[0x2714]);
		case PackExt("FTC"):
			return DecodeFtc(content, contentLength);
		case PackExt("FUN"):
		case PackExt("FP2"):
			return DecodeFun(content, contentLength);
		case PackExt("FWA"):
			return DecodeFwa(content, contentLength);
		case PackExt("GG"):
			return DecodeGg(content, contentLength);
		case PackExt("GHG"):
			return DecodeGhg(content, contentLength);
		case PackExt("GOD"):
			return DecodeGod(content, contentLength);
		case PackExt("G10"):
			return DecodeG10(content, contentLength);
		case PackExt("G11"):
			return DecodeG11(content, contentLength);
		case PackExt("GFB"):
			return DecodeGfb(content, contentLength);
		case PackExt("GR7"):
			return DecodeGr7(content, 0, contentLength);
		case PackExt("GR8"):
			return DecodeGr8(content, contentLength);
		case PackExt("GR9"):
			return DecodeGr9(content, contentLength);
		case PackExt("GUN"):
			return DecodeGun(content, contentLength);
		case PackExt("HED"):
			return DecodeHed(content, contentLength);
		case PackExt("HFC"):
			return DecodeHfc(content, contentLength);
		case PackExt("HGB"):
			return DecodeHgb(content, contentLength);
		case PackExt("HGR"):
			return DecodeHgr(content, contentLength);
		case PackExt("HIM"):
			return DecodeHim(content, contentLength);
		case PackExt("HIP"):
			return DecodeHip(content, contentLength);
		case PackExt("HIR"):
		case PackExt("HBM"):
		case PackExt("GIH"):
		case PackExt("FGS"):
		case PackExt("MON"):
		case PackExt("GCD"):
			return DecodeHir(content, contentLength);
		case PackExt("HLF"):
			return DecodeHlf(content, contentLength);
		case PackExt("HLR"):
			return DecodeHlr(content, contentLength);
		case PackExt("HPC"):
			return DecodeIph(content, contentLength);
		case PackExt("HPM"):
			return DecodeHpm(content, contentLength);
		case PackExt("HR"):
			return DecodeHr(content, contentLength);
		case PackExt("HR2"):
		case PackExt("HCI"):
			return DecodeHr2(content, contentLength);
		case PackExt("ICE"):
			return contentLength > 2048 && DecodeAtari8Ice(content, contentLength, true, content[0]);
		case PackExt("ICN"):
			return DecodeStIcn(content, contentLength)
				|| DecodeAtari8Ice(content, contentLength, false, 17);
		case PackExt("IFF"):
		case PackExt("ACBM"):
		case PackExt("BL1"):
		case PackExt("BL2"):
		case PackExt("BL3"):
		case PackExt("HAM6"):
		case PackExt("HAM8"):
		case PackExt("LBM"):
			return DecodeIff(content, contentLength);
		case PackExt("IFL"):
			return DecodeGun(content, contentLength)
				|| DecodeZxIfl(content, contentLength);
		case PackExt("IGE"):
			return DecodeIge(content, contentLength);
		case PackExt("IHE"):
			return DecodeIhe(content, contentLength);
		case PackExt("IMG"):
			return DecodeStImg(content, contentLength)
				|| DecodeZxImg(content, contentLength);
		case PackExt("XIMG"):
			return DecodeStImg(content, contentLength);
		case PackExt("IMN"):
			return DecodeAtari8Ice(content, contentLength, false, 18);
		case PackExt("INFO"):
			return DecodeInfo(content, contentLength);
		case PackExt("ING"):
		case PackExt("INP"):
			return DecodeInp(content, contentLength);
		case PackExt("INT"):
			return DecodeInt(content, contentLength)
				|| DecodeInp(content, contentLength);
		case PackExt("IP2"):
			return DecodeIp2(content, contentLength);
		case PackExt("IPC"):
			return DecodeAtari8Ice(content, contentLength, false, 19);
		case PackExt("IPT"):
			return contentLength == 10003 && DecodeC64Multicolor(320, content, 2, 0x1f42, 0x232a, content[0x2712]);
		case PackExt("IR2"):
			return DecodeAtari8Ice(content, contentLength, false, 2);
		case PackExt("IRG"):
			return DecodeAtari8Ice(content, contentLength, false, 1);
		case PackExt("ISM"):
			return contentLength == 10218 && DecodeC64Multicolor(320, content, 0x402, 0x2402, 2, content[0x2401]);
		case PackExt("IST"):
			return DecodeIst(content, contentLength);
		case PackExt("JGP"):
			return DecodeJgp(content, contentLength);
		case PackExt("JJ"):
			return DecodeJj(content, contentLength);
		case PackExt("KOA"):
		case PackExt("GIG"):
		case PackExt("RPM"):
			return DecodeKoa(content, contentLength);
		case PackExt("LEO"):
			return DecodeLeo(content, contentLength);
		case PackExt("LP3"):
			return DecodeLp3(content, contentLength);
		case PackExt("LUM"):
			return DecodeLum(content, contentLength);
		case PackExt("MAC"):
			return DecodeMac(content, contentLength);
		case PackExt("MAX"):
			return DecodeMax(content, contentLength);
		case PackExt("MBG"):
			return DecodeMbg(content, contentLength);
		case PackExt("MC"):
			return DecodeMc(content, contentLength);
		case PackExt("MCH"):
			return DecodeMch(content, contentLength);
		case PackExt("MCI"):
			return DecodeMci(content, contentLength);
		case PackExt("MCP"):
			return DecodeMcp(content, contentLength);
		case PackExt("MCPP"):
			return DecodeMcpp(content, contentLength);
		case PackExt("MCS"):
			return DecodeMcs(content, contentLength);
		case PackExt("MG1"):
		case PackExt("MG2"):
		case PackExt("MG4"):
		case PackExt("MG8"):
			return DecodeMg(content, contentLength);
		case PackExt("MGP"):
			return DecodeMgp(content, contentLength);
		case PackExt("MIC"):
			return DecodeMic(content, contentLength);
		case PackExt("MIL"):
			return contentLength == 10022 && DecodeC64Multicolor(320, content, 0x7e6, 0x16, 0x3fe, 0);
		case PackExt("MIS"):
			return DecodeMis(content, contentLength);
		case PackExt("MLE"):
			return DecodeMle(content, contentLength);
		case PackExt("MPP"):
			return DecodeMpp(content, contentLength);
		case PackExt("MSP"):
			return DecodeMsp(content, contentLength);
		case PackExt("NEO"):
			return DecodeNeo(content, contentLength);
		case PackExt("NLQ"):
			return DecodeNlq(content, contentLength);
		case PackExt("OCP"):
			return DecodeOcp(content, contentLength);
		case PackExt("P"):
			return DecodeP(content, contentLength);
		case PackExt("P4I"):
			return DecodeP4i(content, contentLength);
		case PackExt("P64"):
			return contentLength == 10050 && DecodeC64Multicolor(320, content, 0x802, 0x402, 2, content[0x801]);
		case PackExt("PAC"):
			return DecodePac(content, contentLength);
		case PackExt("PC1"):
		case PackExt("PC2"):
		case PackExt("PC3"):
			return DecodePc(content, contentLength);
		case PackExt("PCS"):
			return DecodePcs(content, contentLength);
		case PackExt("PGC"):
			return DecodePgc(content, contentLength);
		case PackExt("PGF"):
			return DecodePgf(content, contentLength);
		case PackExt("PI"):
			return contentLength == 10242 && DecodeC64Multicolor(320, content, 2, 0x2002, 0x2402, content[0x1f82]);
		case PackExt("PI1"):
		case PackExt("PI2"):
		case PackExt("PI3"):
			return DecodeStPi(content, contentLength);
		case PackExt("PI4"):
		case PackExt("PI9"):
			return DecodePi4(content, contentLength);
		case PackExt("PIC"):
			return DecodePic(content, contentLength);
		case PackExt("PL4"):
			return DecodePl4(content, contentLength);
		case PackExt("PLA"):
			return DecodePla(content, contentLength);
		case PackExt("PLM"):
			return DecodeApc(content, contentLength);
		case PackExt("PMD"):
			return DecodePmd(content, contentLength);
		case PackExt("PMG"):
			return DecodePmg(content, contentLength);
		case PackExt("RAW"):
			return DecodeRaw(content, contentLength);
		case PackExt("RGB"):
			return DecodeAtari8Rgb(content, contentLength)
				|| DecodeZxRgb(content, contentLength);
		case PackExt("RIP"):
			return DecodeRip(content, contentLength);
		case PackExt("RM0"):
			return DecodeRm(content, contentLength, 0, RECOILResolution.Xe2x2);
		case PackExt("RM1"):
			return DecodeRm(content, contentLength, 1, RECOILResolution.Xe4x1);
		case PackExt("RM2"):
			return DecodeRm(content, contentLength, 2, RECOILResolution.Xe4x1);
		case PackExt("RM3"):
			return DecodeRm(content, contentLength, 3, RECOILResolution.Xe4x1);
		case PackExt("RM4"):
			return DecodeRm(content, contentLength, 4, RECOILResolution.Xe2x1);
		case PackExt("RP"):
			return contentLength == 10242 && DecodeC64Multicolor(320, content, 0x402, 2, 0x2402, 0);
		case PackExt("RYS"):
			return DecodeRys(content, contentLength);
		case PackExt("SAR"):
			return contentLength == 10018 && DecodeC64Multicolor(320, content, 0x402, 2, 0x2402, content[0x3f2]);
		case PackExt("SC2"):
		case PackExt("GRP"):
			return DecodeSc2(content, contentLength);
		case PackExt("SC8"):
			return DecodeSc8(content, contentLength);
		case PackExt("SCA"):
			return DecodeSca(content, contentLength);
		case PackExt("SCC"):
		case PackExt("SRS"):
			return DecodeScc(content, contentLength);
		case PackExt("SCR"):
			return DecodeScr(content, contentLength);
		case PackExt("SCS4"):
			return DecodeScs4(content, contentLength);
		case PackExt("SGE"):
			return DecodeSge(content, contentLength);
		case PackExt("SHC"):
			return DecodeShc(content, contentLength);
		case PackExt("SHP"):
			return DecodeShp(content, contentLength);
		case PackExt("SHR"):
			return DecodeShr(content, contentLength);
		case PackExt("SIF"):
			return DecodeSif(content, contentLength);
		case PackExt("SPC"):
			return DecodeStSpc(content, contentLength)
				|| DecodeAtari8Spc(content, contentLength);
		case PackExt("SPS"):
			return DecodeSps(content, contentLength);
		case PackExt("SPU"):
			return DecodeSpu(content, contentLength);
		case PackExt("SR8"):
			return DecodeSr8(content, contentLength);
		case PackExt("SXS"):
			return DecodeSxs(content, contentLength);
		case PackExt("TIP"):
			return DecodeTip(content, contentLength);
		case PackExt("TN1"):
		case PackExt("TN2"):
		case PackExt("TN3"):
		case PackExt("TNY"):
			return DecodeTny(content, contentLength);
		case PackExt("TRP"):
			return DecodeTrp(content, contentLength);
		case PackExt("TRU"):
			return DecodeTru(content, contentLength);
		case PackExt("VIC"):
			return DecodeVic(content, contentLength);
		case PackExt("VZI"):
			return DecodeVzi(content, contentLength);
		case PackExt("WND"):
			return DecodeWnd(content, contentLength);
		case PackExt("XLP"):
			return DecodeXlp(content, contentLength);
		case PackExt("ZXP"):
			return DecodeZxp(content, contentLength);
		default:
			return false;
		}
	}

	// Number of unique colors in `pixels`.
	int Colors;
	const int UnknownColors = -1;
	// Special case when palette-indexed conversion fails.
	const int MoreThan256Colors = 0;

	// Unique colors.
	// Values are in ascending order for the sake of binary search.
	int[256] Palette;

	/// Find the `rgb` value in `palette`.
	/// Searches `paletteEnd` entries.
	static int FindColor(int[] palette, int paletteEnd, int rgb)
	{
		int paletteBegin = 0;
		while (paletteBegin < paletteEnd) {
			int index = (paletteBegin + paletteEnd) >> 1;
			int paletteRgb = palette[index];
			if (rgb == paletteRgb)
				return index;
			if (rgb < paletteRgb)
				paletteEnd = index;
			else
				paletteBegin = index + 1;
		}
		return paletteBegin;
	}

	/// Calculates palette for the decoded picture.
	/// On entry, `palette` contains `paletteOffset` colors.
	void CalculatePalette(int[] palette, int paletteOffset, int paletteLength)
	{
		int pixelsCount = Width * Height;
		for (int pixelsOffset = 0; pixelsOffset < pixelsCount; pixelsOffset++) {
			int rgb = Pixels[pixelsOffset];
			int index = FindColor(palette, paletteOffset, rgb);
			if (index >= paletteOffset || palette[index] != rgb) {
				if (paletteOffset == paletteLength) {
					Colors = MoreThan256Colors;
					return;
				}
				for (int j = paletteOffset; --j >= index; )
					palette[j + 1] = palette[j];
				palette[index] = rgb;
				paletteOffset++;
			}
		}
		Colors = paletteOffset;
	}

	/// Returns number of unique colors in the decoded picture.
	public int GetColors()
	{
		if (Colors == UnknownColors) {
			Palette.Clear();
			CalculatePalette(Palette, 0, 256);
		}
		if (Colors == MoreThan256Colors) {
			int[] palette = new int[Width * Height];
			// Copy already found colors
			// - will be faster than starting from scratch.
			for (int j = 0; j < 256; j++)
				palette[j] = Palette[j];
			CalculatePalette(palette, 256, -1);
			delete palette;
		}
		return Colors;
	}

	/// Converts the decoded picture to palette-indexed.
	/// Returns palette of 256 0xRRGGBB entries.
	/// Call `GetColors()` for the actual number of colors.
	/// Returns `null` if conversion fails,
	/// because there are more than 256 colors.
	public int[] ToPalette(
		/// Out: palette-indexed picture.
		/// It must be caller-allocated of length `GetWidth() * GetHeight()`.
		byte[] indexes)
	{
		if (Colors == UnknownColors) {
			Palette.Clear();
			CalculatePalette(Palette, 0, 256);
		}
		if (Colors == MoreThan256Colors || Colors > 256)
			return null;
		int pixelsCount = Width * Height;
		for (int i = 0; i < pixelsCount; i++)
			indexes[i] = FindColor(Palette, Colors, Pixels[i]);
		return Palette;
	}
}
