437 lines
16 KiB
C#

//! \file ImagePICT.cs
//! \date 2023 Aug 24
//! \brief Macintosh picture format.
//
// Copyright (C) 2023 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
using GameRes.Utility;
using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace GameRes.Formats.Apple
{
internal class PictMetaData : ImageMetaData
{
public uint DataOffset;
}
[Export(typeof(ImageFormat))]
public class PictFormat : ImageFormat
{
public override string Tag { get => "PICT/MAC"; }
public override string Description { get => "Apple Macintosh image format"; }
public override uint Signature { get => 0; }
public PictFormat ()
{
Signatures = new[] { 0u, 0x54434950u };
Extensions = new[] { "pct", "pict", "pic" };
}
public override ImageMetaData ReadMetaData (IBinaryStream file)
{
int header_pos = 0x200;
if (file.Signature == 0x54434950) // 'PICT'
header_pos = 4;
if (file.Length < header_pos + 0x10)
return null;
file.Position = header_pos + 2;
short top = file.ReadI16BE();
short left = file.ReadI16BE();
short bottom = file.ReadI16BE();
short right = file.ReadI16BE();
if (file.ReadU16BE() != 0x11)
return null;
int version = file.ReadU16BE();
if (version != 0x2FF)
return null;
return new PictMetaData {
Width = (uint)(right - left),
Height = (uint)(bottom - top),
OffsetX = left,
OffsetY = top,
BPP = 32,
DataOffset = (uint)file.Position,
};
}
public override ImageData Read (IBinaryStream file, ImageMetaData info)
{
var decoder = new PictReader (file, (PictMetaData)info);
var pixels = decoder.Unpack();
return ImageData.Create (info, decoder.Format, decoder.Palette, pixels);
}
public override void Write (Stream file, ImageData image)
{
throw new System.NotImplementedException ("PictFormat.Write not implemented");
}
}
internal static class BinaryStreamExtension
{
static public short ReadI16BE (this IBinaryStream file)
{
return Binary.BigEndian (file.ReadInt16());
}
static public ushort ReadU16BE (this IBinaryStream file)
{
return Binary.BigEndian (file.ReadUInt16());
}
static public int ReadI32BE (this IBinaryStream file)
{
return Binary.BigEndian (file.ReadInt32());
}
static public uint ReadU32BE (this IBinaryStream file)
{
return Binary.BigEndian (file.ReadUInt32());
}
}
internal class Pixmap
{
public short Version;
public short PackType;
public int PackSize;
public int HorizRes;
public int VertRes;
public short PixelType;
public short BPP;
public short CompCount;
public short CompSize;
public int PlaneBytes;
public int Table;
public void Deserialize (IBinaryStream input)
{
Version = input.ReadI16BE();
PackType = input.ReadI16BE();
PackSize = input.ReadI32BE();
HorizRes = input.ReadI32BE() >> 16; // read 2 bytes and skip next 2
VertRes = input.ReadI32BE() >> 16;
PixelType = input.ReadI16BE();
BPP = input.ReadI16BE();
CompCount = input.ReadI16BE();
CompSize = input.ReadI16BE();
PlaneBytes = input.ReadI32BE();
Table = input.ReadI32BE();
input.Seek (4, SeekOrigin.Current);
if (BPP <= 0 || BPP > 32 || CompCount <= 0 || CompCount > 4 || CompSize <= 0)
throw new InvalidFormatException();
}
}
internal class PictReader
{
IBinaryStream m_input;
PictMetaData m_info;
public PixelFormat Format { get; private set; }
public BitmapPalette Palette { get; private set; }
public PictReader (IBinaryStream input, PictMetaData info)
{
m_input = input;
m_info = info;
}
bool HasAlpha = false;
byte[] m_buffer;
public byte[] Unpack ()
{
Color[] colormap = null;
Pixmap pixmap = null;
m_input.Position = m_info.DataOffset;
while (m_input.PeekByte() != -1)
{
if ((m_input.Position & 1) != 0)
Skip (1);
int code = m_input.ReadU16BE();
if (0x00FF == code || 0xFFFF == code) // EOF
break;
switch (code)
{
case 0x0000: // NOP
continue;
case 0x0001: // Clip
{
int length = m_input.ReadU16BE();
if (length < 2)
throw new InvalidFormatException();
Skip (length-2);
break;
}
case 0x0090:
case 0x0091:
case 0x0098:
case 0x0099:
case 0x009A:
case 0x009B: // BitsRect
{
int stride = 0;
if (code != 0x9A && code != 0x9B)
stride = m_input.ReadU16BE();
else
Skip (6);
// FIXME we just read the first bitmap and override an existing frame
// TODO place bitmap into frame according to its RECT
m_info.OffsetY = m_input.ReadI16BE();
m_info.OffsetX = m_input.ReadI16BE();
m_info.Height = (uint)(m_input.ReadI16BE() - m_info.OffsetY);
m_info.Width = (uint)(m_input.ReadI16BE() - m_info.OffsetX);
if (0x9A == code || 0x9B == code || (stride & 0x8000) != 0)
{
pixmap = new Pixmap();
pixmap.Deserialize (m_input);
HasAlpha = pixmap.CompCount == 4;
}
if (code != 0x9A && code != 0x9B)
{
int colors = 2;
int flags = 0;
if ((stride & 0x8000) != 0)
{
Skip (4);
flags = m_input.ReadU16BE();
colors = m_input.ReadU16BE() + 1;
}
if (null == colormap)
colormap = new Color[colors];
if ((stride & 0x8000) != 0)
{
for (int i = 0; i < colors; i++)
{
int c = m_input.ReadU16BE() % colors;
if ((flags & 0x8000) != 0)
c = i;
int r = m_input.ReadU16BE() / 0x101;
int g = m_input.ReadU16BE() / 0x101;
int b = m_input.ReadU16BE() / 0x101;
colormap[c] = Color.FromRgb ((byte)r, (byte)g, (byte)b);
}
}
else
{
var White = Color.FromRgb (0xFF, 0xFF, 0xFF);
for (int i = 0; i < colors; i++)
{
colormap[i] = Color.Subtract (White, colormap[i]);
}
}
}
Skip (8+8+2);
// -> Skip (8); // source RECT
// Skip (8); // destination RECT
// Skip (2); // transfer mode
if (code == 0x91 || code == 0x99 || code == 0x9b)
{
int length = m_input.ReadU16BE();
if (length > 2)
Skip (length - 2);
}
if (code != 0x9A && code != 0x9B && (stride & 0x8000) == 0)
DecodeRleBitmap (stride, 1);
else
DecodeRleBitmap (stride, pixmap.BPP);
break;
}
case 0x00A1: // LongComment
{
m_input.ReadU16BE(); // comment type
int length = m_input.ReadU16BE();
Skip (length);
break;
}
case 0x0C00: // Header
Skip (0x18);
break;
default:
throw new NotSupportedException (string.Format ("Unknown code 0x{0:X4} in PICT stream.", code));
}
}
if (colormap != null)
Palette = new BitmapPalette (colormap);
if (null == m_buffer)
throw new InvalidFormatException();
SetFormat (pixmap);
return RepackPixels (pixmap);
}
byte[] RepackPixels (Pixmap pixmap)
{
int bpp = m_info.BPP;
if (bpp <= 16)
return m_buffer;
int bytes_per_pixel = bpp / 8;
int stride = m_info.iWidth * bytes_per_pixel;
var pixels = new byte[stride * m_info.iHeight];
int src = 0;
for (int y = 0; y < m_info.iHeight; ++y)
{
int dst = y * stride;
for (int x = 0; x < m_info.iWidth; ++x)
{
if (HasAlpha)
{
pixels[dst+3] = m_buffer[src];
pixels[dst+2] = m_buffer[src+m_info.iWidth];
pixels[dst+1] = m_buffer[src+m_info.iWidth*2];
pixels[dst] = m_buffer[src+m_info.iWidth*3];
}
else
{
pixels[dst+2] = m_buffer[src];
pixels[dst+1] = m_buffer[src+m_info.iWidth];
pixels[dst] = m_buffer[src+m_info.iWidth*2];
}
++src;
dst += bytes_per_pixel;
}
src += (pixmap.CompCount - 1) * m_info.iWidth;
}
return pixels;
}
void SetFormat (Pixmap pixmap)
{
int bpp = null == pixmap ? 8 : pixmap.BPP;
if (32 == bpp)
{
if (4 == pixmap.CompCount)
Format = PixelFormats.Bgra32;
else
Format = PixelFormats.Bgr32;
}
else if (24 == bpp)
Format = PixelFormats.Bgr24;
else if (16 == bpp)
Format = PixelFormats.Bgr555;
else if (8 == bpp)
{
if (Palette != null)
Format = PixelFormats.Indexed8;
else
Format = PixelFormats.Gray8;
}
else
throw new NotSupportedException (string.Format ("Not supported PICT bitdepth -- {0}bpp", bpp));
m_info.BPP = bpp;
}
void Skip (int amount)
{
m_input.Seek (amount, SeekOrigin.Current);
}
byte[] m_unpack_buffer = new byte[0x800];
byte[] m_scanline;
void DecodeRleBitmap (int stride, int bpp)
{
if (bpp < 8)
throw new NotSupportedException();
if (bpp <= 8)
stride &= 0x7fff;
int width = m_info.iWidth;
int bytes_per_pixel = 1;
if (16 == bpp)
{
bytes_per_pixel = 2;
width *= 2;
}
else if (32 == bpp)
width *= HasAlpha ? 4 : 3;
if (stride == 0)
stride = width;
int stride_32bpp = m_info.iWidth * 4;
int total_bytes = stride_32bpp * m_info.iHeight;
if (null == m_buffer || m_buffer.Length < total_bytes)
m_buffer = new byte[total_bytes];
int scanline_length = stride_32bpp * 2;
if (null == m_scanline || m_scanline.Length < scanline_length)
{
m_scanline = new byte[scanline_length];
}
if (stride < 8)
{
int dst = 0;
int row_size = width * (bpp / 8);
for (int y = 0; y < m_info.iHeight; ++y)
{
m_input.Read (m_buffer, dst, stride);
dst += row_size;
}
return;
}
for (int y = 0; y < m_info.iHeight; ++y)
{
int dst = y * width;
if (stride > 200)
scanline_length = m_input.ReadU16BE();
else
scanline_length = m_input.ReadUInt8();
if (scanline_length >= m_scanline.Length || scanline_length == 0)
throw new InvalidFormatException();
m_input.Read (m_scanline, 0, scanline_length);
for (int j = 0; j < scanline_length; )
{
if ((m_scanline[j] & 0x80) == 0)
{
int pixel_count = m_scanline[j] + 1;
int count = pixel_count * bytes_per_pixel;
int src = j + 1;
if ((dst + count) <= total_bytes)
Buffer.BlockCopy (m_scanline, src, m_buffer, dst, count);
dst += count;
j += count + 1;
}
else
{
int count = (m_scanline[j] ^ 0xFF) + 2;
int src = j + 1;
while (count --> 0)
{
if ((dst + bytes_per_pixel) <= total_bytes)
Buffer.BlockCopy (m_scanline, src, m_buffer, dst, bytes_per_pixel);
dst += bytes_per_pixel;
}
j += bytes_per_pixel + 1;
}
}
}
}
}
}