Skip to content

Commit

Permalink
PNG spritesheet support, along with PaletteFromPng.
Browse files Browse the repository at this point in the history
  • Loading branch information
IceReaper committed Sep 26, 2018
1 parent 09d8aaf commit b4bcd1c
Show file tree
Hide file tree
Showing 14 changed files with 673 additions and 294 deletions.
211 changes: 211 additions & 0 deletions OpenRA.Game/FileFormats/Png.cs
@@ -0,0 +1,211 @@
#region Copyright & License Information
/*
* Copyright 2007-2018 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;

namespace OpenRA.FileFormats
{
public class Png
{
public int Width { get; set; }
public int Height { get; set; }
public PixelFormat PixelFormat { get; set; }
public Color[] Palette { get; set; }
public byte[] Data { get; set; }
public Dictionary<string, string> Meta = new Dictionary<string, string>();

public Png(Stream s)
{
if (!Verify(s))
throw new InvalidDataException("PNG Signature is bogus");

s.Position += 8;
var headerParsed = false;

using (var br = new BinaryReader(s))
{
var data = new List<byte>();

for (;;)
{
var length = IPAddress.NetworkToHostOrder(br.ReadInt32());
var type = Encoding.UTF8.GetString(br.ReadBytes(4));
var content = br.ReadBytes(length);
/*var crc = */br.ReadInt32();

if (!headerParsed && type != "IHDR")
throw new InvalidDataException("Invalid PNG file - header does not appear first.");

using (var ms = new MemoryStream(content))
using (var cr = new BinaryReader(ms))
switch (type)
{
case "IHDR":
{
if (headerParsed)
throw new InvalidDataException("Invalid PNG file - duplicate header.");

Width = IPAddress.NetworkToHostOrder(cr.ReadInt32());
Height = IPAddress.NetworkToHostOrder(cr.ReadInt32());
Data = new byte[Width * Height];

var bitDepth = cr.ReadByte();
var colorType = (PngColorType)cr.ReadByte();
PixelFormat = MakePixelFormat(bitDepth, colorType);

var compression = cr.ReadByte();
/*var filter = */cr.ReadByte();
var interlace = cr.ReadByte();

if (compression != 0) throw new InvalidDataException("Compression method not supported");
if (interlace != 0) throw new InvalidDataException("Interlacing not supported");

headerParsed = true;
}

break;

case "PLTE":
{
Palette = new Color[256];
for (var i = 0; i < length / 3; i++)
{
var r = cr.ReadByte(); var g = cr.ReadByte(); var b = cr.ReadByte();
Palette[i] = Color.FromArgb(r, g, b);
}
}

break;

case "tRNS":
{
if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");

for (var i = 0; i < length; i++)
Palette[i] = Color.FromArgb(cr.ReadByte(), Palette[i]);
}

break;

case "IDAT":
{
data.AddRange(content);
}

break;

case "tEXt":
{
var key = new List<byte>();
byte b;

while ((b = cr.ReadByte()) != 0x00)
key.Add(b);

Meta.Add(Encoding.ASCII.GetString(key.ToArray()), Encoding.ASCII.GetString(cr.ReadBytes(length - key.Count - 1)));
}

break;

case "IEND":
{
using (var ns = new MemoryStream(data.ToArray()))
{
// 'zlib' flags bytes; confuses the DeflateStream.
/*var flags = (byte)*/ns.ReadByte();
/*var moreFlags = (byte)*/ns.ReadByte();

using (var ds = new DeflateStream(ns, CompressionMode.Decompress))
using (var dr = new BinaryReader(ds))
{
var prevLine = new byte[Width]; // all zero
for (var y = 0; y < Height; y++)
{
var filter = (PngFilter)dr.ReadByte();
var line = dr.ReadBytes(Width);

for (var i = 0; i < Width; i++)
line[i] = i > 0
? UnapplyFilter(filter, line[i], line[i - 1], prevLine[i], prevLine[i - 1])
: UnapplyFilter(filter, line[i], 0, prevLine[i], 0);

Array.Copy(line, 0, Data, y * Width, line.Length);
prevLine = line;
}
}
}

if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
}

return;
}
}
}
}

public static bool Verify(Stream s)
{
var pos = s.Position;
var signature = new[] { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
var isPng = signature.Aggregate(true, (current, t) => current && s.ReadUInt8() == t);
s.Position = pos;
return isPng;
}

static byte UnapplyFilter(PngFilter f, byte x, byte a, byte b, byte c)
{
switch (f)
{
case PngFilter.None: return x;
case PngFilter.Sub: return (byte)(x + a);
case PngFilter.Up: return (byte)(x + b);
case PngFilter.Average: return (byte)(x + (a + b) / 2);
case PngFilter.Paeth: return (byte)(x + Paeth(a, b, c));
default:
throw new InvalidOperationException("Unsupported Filter");
}
}

static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);

return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}

[Flags]
enum PngColorType { Indexed = 1, Color = 2, Alpha = 4 }
enum PngFilter { None, Sub, Up, Average, Paeth }

static PixelFormat MakePixelFormat(byte bitDepth, PngColorType colorType)
{
if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
return PixelFormat.Format8bppIndexed;

throw new InvalidDataException("Unknown pixel format");
}
}
}

0 comments on commit b4bcd1c

Please sign in to comment.