PNG spritesheet support, along with PaletteFromPng.
Cursor palette loader can now be specified via yaml.
This commit is contained in:
@@ -1,206 +0,0 @@
|
|||||||
#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.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace OpenRA.FileFormats
|
|
||||||
{
|
|
||||||
public static class PngLoader
|
|
||||||
{
|
|
||||||
public static Bitmap Load(string filename)
|
|
||||||
{
|
|
||||||
using (var s = File.OpenRead(filename))
|
|
||||||
return Load(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Bitmap Load(Stream s)
|
|
||||||
{
|
|
||||||
using (var br = new BinaryReader(s))
|
|
||||||
{
|
|
||||||
var signature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 };
|
|
||||||
foreach (var b in signature)
|
|
||||||
if (br.ReadByte() != b)
|
|
||||||
throw new InvalidDataException("PNG Signature is bogus");
|
|
||||||
|
|
||||||
Bitmap bitmap = null;
|
|
||||||
Color[] palette = null;
|
|
||||||
var data = new List<byte>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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 (bitmap == null && 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 (bitmap != null)
|
|
||||||
throw new InvalidDataException("Invalid PNG file - duplicate header.");
|
|
||||||
|
|
||||||
var width = IPAddress.NetworkToHostOrder(cr.ReadInt32());
|
|
||||||
var height = IPAddress.NetworkToHostOrder(cr.ReadInt32());
|
|
||||||
var bitDepth = cr.ReadByte();
|
|
||||||
var colorType = (PngColorType)cr.ReadByte();
|
|
||||||
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");
|
|
||||||
|
|
||||||
bitmap = new Bitmap(width, height, MakePixelFormat(bitDepth, colorType));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 "IEND":
|
|
||||||
{
|
|
||||||
var bits = bitmap.LockBits(bitmap.Bounds(),
|
|
||||||
ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
|
|
||||||
|
|
||||||
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[bitmap.Width]; // all zero
|
|
||||||
for (var y = 0; y < bitmap.Height; y++)
|
|
||||||
{
|
|
||||||
var filter = (PngFilter)dr.ReadByte();
|
|
||||||
var line = dr.ReadBytes(bitmap.Width);
|
|
||||||
|
|
||||||
for (var i = 0; i < bitmap.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);
|
|
||||||
|
|
||||||
Marshal.Copy(line, 0, new IntPtr(bits.Scan0.ToInt64() + y * bits.Stride), line.Length);
|
|
||||||
prevLine = line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bitmap.UnlockBits(bits);
|
|
||||||
|
|
||||||
if (palette == null)
|
|
||||||
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
|
||||||
|
|
||||||
using (var temp = new Bitmap(1, 1, PixelFormat.Format8bppIndexed))
|
|
||||||
{
|
|
||||||
var cp = temp.Palette;
|
|
||||||
for (var i = 0; i < 256; i++)
|
|
||||||
cp.Entries[i] = palette[i]; // finalize the palette.
|
|
||||||
bitmap.Palette = cp;
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (bitmap != null)
|
|
||||||
bitmap.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -273,7 +273,6 @@
|
|||||||
<Compile Include="FieldSaver.cs" />
|
<Compile Include="FieldSaver.cs" />
|
||||||
<Compile Include="Manifest.cs" />
|
<Compile Include="Manifest.cs" />
|
||||||
<Compile Include="Graphics\Vertex.cs" />
|
<Compile Include="Graphics\Vertex.cs" />
|
||||||
<Compile Include="FileFormats\PngLoader.cs" />
|
|
||||||
<Compile Include="Primitives\ActionQueue.cs" />
|
<Compile Include="Primitives\ActionQueue.cs" />
|
||||||
<Compile Include="Primitives\BitSet.cs" />
|
<Compile Include="Primitives\BitSet.cs" />
|
||||||
<Compile Include="Primitives\Cache.cs" />
|
<Compile Include="Primitives\Cache.cs" />
|
||||||
|
|||||||
205
OpenRA.Mods.Common/FileFormats/Png.cs
Normal file
205
OpenRA.Mods.Common/FileFormats/Png.cs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
#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> EmbeddedData = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public Png(Stream s)
|
||||||
|
{
|
||||||
|
if (!Verify(s))
|
||||||
|
throw new InvalidDataException("PNG Signature is bogus");
|
||||||
|
|
||||||
|
s.Position += 8;
|
||||||
|
var headerParsed = false;
|
||||||
|
|
||||||
|
var data = new List<byte>();
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
|
||||||
|
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
|
||||||
|
var content = s.ReadBytes(length);
|
||||||
|
/*var crc = */s.ReadInt32();
|
||||||
|
|
||||||
|
if (!headerParsed && type != "IHDR")
|
||||||
|
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
|
||||||
|
|
||||||
|
using (var ms = new MemoryStream(content))
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "IHDR":
|
||||||
|
{
|
||||||
|
if (headerParsed)
|
||||||
|
throw new InvalidDataException("Invalid PNG file - duplicate header.");
|
||||||
|
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||||
|
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||||
|
Data = new byte[Width * Height];
|
||||||
|
|
||||||
|
var bitDepth = ms.ReadUInt8();
|
||||||
|
var colorType = (PngColorType)ms.ReadByte();
|
||||||
|
PixelFormat = MakePixelFormat(bitDepth, colorType);
|
||||||
|
|
||||||
|
var compression = ms.ReadByte();
|
||||||
|
/*var filter = */ms.ReadByte();
|
||||||
|
var interlace = ms.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 = ms.ReadByte(); var g = ms.ReadByte(); var b = ms.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(ms.ReadByte(), Palette[i]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "IDAT":
|
||||||
|
{
|
||||||
|
data.AddRange(content);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tEXt":
|
||||||
|
{
|
||||||
|
var key = ms.ReadASCIIZ();
|
||||||
|
EmbeddedData.Add(key, ms.ReadASCII(length - key.Length - 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))
|
||||||
|
{
|
||||||
|
var prevLine = new byte[Width]; // all zero
|
||||||
|
for (var y = 0; y < Height; y++)
|
||||||
|
{
|
||||||
|
var filter = (PngFilter)ds.ReadByte();
|
||||||
|
var line = ds.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,6 +146,7 @@
|
|||||||
<Compile Include="Effects\MapNotificationEffect.cs" />
|
<Compile Include="Effects\MapNotificationEffect.cs" />
|
||||||
<Compile Include="Effects\RallyPointIndicator.cs" />
|
<Compile Include="Effects\RallyPointIndicator.cs" />
|
||||||
<Compile Include="Effects\SpriteEffect.cs" />
|
<Compile Include="Effects\SpriteEffect.cs" />
|
||||||
|
<Compile Include="FileFormats\Png.cs" />
|
||||||
<Compile Include="Graphics\RailgunRenderable.cs" />
|
<Compile Include="Graphics\RailgunRenderable.cs" />
|
||||||
<Compile Include="Effects\LaunchEffect.cs" />
|
<Compile Include="Effects\LaunchEffect.cs" />
|
||||||
<Compile Include="Lint\CheckNotifications.cs" />
|
<Compile Include="Lint\CheckNotifications.cs" />
|
||||||
@@ -559,6 +560,7 @@
|
|||||||
<Compile Include="Traits\World\MPStartUnits.cs" />
|
<Compile Include="Traits\World\MPStartUnits.cs" />
|
||||||
<Compile Include="Traits\World\PaletteFromFile.cs" />
|
<Compile Include="Traits\World\PaletteFromFile.cs" />
|
||||||
<Compile Include="Traits\World\PaletteFromGimpOrJascFile.cs" />
|
<Compile Include="Traits\World\PaletteFromGimpOrJascFile.cs" />
|
||||||
|
<Compile Include="Traits\World\PaletteFromPng.cs" />
|
||||||
<Compile Include="Traits\World\PaletteFromRGBA.cs" />
|
<Compile Include="Traits\World\PaletteFromRGBA.cs" />
|
||||||
<Compile Include="Traits\World\ValidateOrder.cs" />
|
<Compile Include="Traits\World\ValidateOrder.cs" />
|
||||||
<Compile Include="Pathfinder\CellInfoLayerPool.cs" />
|
<Compile Include="Pathfinder\CellInfoLayerPool.cs" />
|
||||||
@@ -596,6 +598,8 @@
|
|||||||
<Compile Include="UtilityCommands\ExtractTraitDocsCommand.cs" />
|
<Compile Include="UtilityCommands\ExtractTraitDocsCommand.cs" />
|
||||||
<Compile Include="UtilityCommands\ExtractWeaponDocsCommand.cs" />
|
<Compile Include="UtilityCommands\ExtractWeaponDocsCommand.cs" />
|
||||||
<Compile Include="Warheads\ChangeOwnerWarhead.cs" />
|
<Compile Include="Warheads\ChangeOwnerWarhead.cs" />
|
||||||
|
<Compile Include="UtilityCommands\PngSheetExportMetadataCommand.cs" />
|
||||||
|
<Compile Include="UtilityCommands\PngSheetImportMetadataCommand.cs" />
|
||||||
<Compile Include="Widgets\Logic\MusicHotkeyLogic.cs" />
|
<Compile Include="Widgets\Logic\MusicHotkeyLogic.cs" />
|
||||||
<Compile Include="WorldExtensions.cs" />
|
<Compile Include="WorldExtensions.cs" />
|
||||||
<Compile Include="UtilityCommands\GetMapHashCommand.cs" />
|
<Compile Include="UtilityCommands\GetMapHashCommand.cs" />
|
||||||
@@ -697,6 +701,7 @@
|
|||||||
<Compile Include="SpriteLoaders\TmpRALoader.cs" />
|
<Compile Include="SpriteLoaders\TmpRALoader.cs" />
|
||||||
<Compile Include="SpriteLoaders\TmpTDLoader.cs" />
|
<Compile Include="SpriteLoaders\TmpTDLoader.cs" />
|
||||||
<Compile Include="SpriteLoaders\ShpD2Loader.cs" />
|
<Compile Include="SpriteLoaders\ShpD2Loader.cs" />
|
||||||
|
<Compile Include="SpriteLoaders\PngSheetLoader.cs" />
|
||||||
<Compile Include="LoadScreens\LogoStripeLoadScreen.cs" />
|
<Compile Include="LoadScreens\LogoStripeLoadScreen.cs" />
|
||||||
<Compile Include="LoadScreens\BlankLoadScreen.cs" />
|
<Compile Include="LoadScreens\BlankLoadScreen.cs" />
|
||||||
<Compile Include="Widgets\Logic\ReplayUtils.cs" />
|
<Compile Include="Widgets\Logic\ReplayUtils.cs" />
|
||||||
|
|||||||
134
OpenRA.Mods.Common/SpriteLoaders/PngSheetLoader.cs
Normal file
134
OpenRA.Mods.Common/SpriteLoaders/PngSheetLoader.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenRA.FileFormats;
|
||||||
|
using OpenRA.Graphics;
|
||||||
|
|
||||||
|
namespace OpenRA.Mods.Common.SpriteLoaders
|
||||||
|
{
|
||||||
|
public class PngSheetLoader : ISpriteLoader
|
||||||
|
{
|
||||||
|
class PngSheetFrame : ISpriteFrame
|
||||||
|
{
|
||||||
|
public Size Size { get; set; }
|
||||||
|
public Size FrameSize { get; set; }
|
||||||
|
public float2 Offset { get; set; }
|
||||||
|
public byte[] Data { get; set; }
|
||||||
|
public bool DisableExportPadding { get { return false; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryParseSprite(Stream s, out ISpriteFrame[] frames)
|
||||||
|
{
|
||||||
|
if (!Png.Verify(s))
|
||||||
|
{
|
||||||
|
frames = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var png = new Png(s);
|
||||||
|
List<Rectangle> frameRegions;
|
||||||
|
List<float2> frameOffsets;
|
||||||
|
|
||||||
|
// Prefer manual defined regions over auto sliced regions.
|
||||||
|
if (png.EmbeddedData.Any(meta => meta.Key.StartsWith("Frame[")))
|
||||||
|
RegionsFromFrames(png, out frameRegions, out frameOffsets);
|
||||||
|
else
|
||||||
|
RegionsFromSlices(png, out frameRegions, out frameOffsets);
|
||||||
|
|
||||||
|
frames = new ISpriteFrame[frameRegions.Count];
|
||||||
|
|
||||||
|
for (var i = 0; i < frames.Length; i++)
|
||||||
|
{
|
||||||
|
var frameStart = frameRegions[i].X + frameRegions[i].Y * png.Width;
|
||||||
|
var frameSize = new Size(frameRegions[i].Width, frameRegions[i].Height);
|
||||||
|
frames[i] = new PngSheetFrame()
|
||||||
|
{
|
||||||
|
Size = frameSize,
|
||||||
|
FrameSize = frameSize,
|
||||||
|
Offset = frameOffsets[i],
|
||||||
|
Data = new byte[frameRegions[i].Width * frameRegions[i].Height]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var y = 0; y < frames[i].Size.Height; y++)
|
||||||
|
Array.Copy(png.Data, frameStart + y * png.Width, frames[i].Data, y * frames[i].Size.Width, frames[i].Size.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegionsFromFrames(Png png, out List<Rectangle> regions, out List<float2> offsets)
|
||||||
|
{
|
||||||
|
regions = new List<Rectangle>();
|
||||||
|
offsets = new List<float2>();
|
||||||
|
|
||||||
|
string frame;
|
||||||
|
for (var i = 0; png.EmbeddedData.TryGetValue("Frame[" + i + "]", out frame); i++)
|
||||||
|
{
|
||||||
|
// Format: x,y,width,height;offsetX,offsetY
|
||||||
|
var coords = frame.Split(';');
|
||||||
|
regions.Add(FieldLoader.GetValue<Rectangle>("Region", coords[0]));
|
||||||
|
offsets.Add(FieldLoader.GetValue<float2>("Offset", coords[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegionsFromSlices(Png png, out List<Rectangle> regions, out List<float2> offsets)
|
||||||
|
{
|
||||||
|
// Default: whole image is 1 frame.
|
||||||
|
var frameSize = new Size(png.Width, png.Height);
|
||||||
|
var frameAmount = 1;
|
||||||
|
|
||||||
|
if (png.EmbeddedData.ContainsKey("FrameSize"))
|
||||||
|
{
|
||||||
|
// If FrameSize exist, use it and...
|
||||||
|
frameSize = FieldLoader.GetValue<Size>("FrameSize", png.EmbeddedData["FrameSize"]);
|
||||||
|
|
||||||
|
// ... either use FrameAmount or calculate how many times FrameSize fits into the image.
|
||||||
|
if (png.EmbeddedData.ContainsKey("FrameAmount"))
|
||||||
|
frameAmount = FieldLoader.GetValue<int>("FrameAmount", png.EmbeddedData["FrameAmount"]);
|
||||||
|
else
|
||||||
|
frameAmount = png.Width / frameSize.Width * png.Height / frameSize.Height;
|
||||||
|
}
|
||||||
|
else if (png.EmbeddedData.ContainsKey("FrameAmount"))
|
||||||
|
{
|
||||||
|
// Otherwise, calculate the number of frames by splitting the image horizontaly by FrameAmount.
|
||||||
|
frameAmount = FieldLoader.GetValue<int>("FrameAmount", png.EmbeddedData["FrameAmount"]);
|
||||||
|
frameSize = new Size(png.Width / frameAmount, png.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 offset;
|
||||||
|
|
||||||
|
// If Offset property exists, use its value. Otherwise assume the frame is centered.
|
||||||
|
if (png.EmbeddedData.ContainsKey("Offset"))
|
||||||
|
offset = FieldLoader.GetValue<float2>("Offset", png.EmbeddedData["Offset"]);
|
||||||
|
else
|
||||||
|
offset = float2.Zero;
|
||||||
|
|
||||||
|
var framesPerRow = png.Width / frameSize.Width;
|
||||||
|
|
||||||
|
regions = new List<Rectangle>();
|
||||||
|
offsets = new List<float2>();
|
||||||
|
|
||||||
|
for (var i = 0; i < frameAmount; i++)
|
||||||
|
{
|
||||||
|
var x = i % framesPerRow * frameSize.Width;
|
||||||
|
var y = i / framesPerRow * frameSize.Height;
|
||||||
|
|
||||||
|
regions.Add(new Rectangle(x, y, frameSize.Width, frameSize.Height));
|
||||||
|
offsets.Add(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
OpenRA.Mods.Common/Traits/World/PaletteFromPng.cs
Normal file
75
OpenRA.Mods.Common/Traits/World/PaletteFromPng.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#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.Collections.Generic;
|
||||||
|
using OpenRA.FileFormats;
|
||||||
|
using OpenRA.Graphics;
|
||||||
|
using OpenRA.Traits;
|
||||||
|
|
||||||
|
namespace OpenRA.Mods.Common.Traits
|
||||||
|
{
|
||||||
|
[Desc("Load a PNG and use its embedded palette.")]
|
||||||
|
class PaletteFromPngInfo : ITraitInfo
|
||||||
|
{
|
||||||
|
[FieldLoader.Require, PaletteDefinition]
|
||||||
|
[Desc("Internal palette name")]
|
||||||
|
public readonly string Name = null;
|
||||||
|
|
||||||
|
[Desc("If defined, load the palette only for this tileset.")]
|
||||||
|
public readonly string Tileset = null;
|
||||||
|
|
||||||
|
[FieldLoader.Require]
|
||||||
|
[Desc("Filename to load")]
|
||||||
|
public readonly string Filename = null;
|
||||||
|
|
||||||
|
[Desc("Map listed indices to shadow. Ignores previous color.")]
|
||||||
|
public readonly int[] ShadowIndex = { };
|
||||||
|
|
||||||
|
public readonly bool AllowModifiers = true;
|
||||||
|
|
||||||
|
public object Create(ActorInitializer init) { return new PaletteFromPng(init.World, this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaletteFromPng : ILoadsPalettes, IProvidesAssetBrowserPalettes
|
||||||
|
{
|
||||||
|
readonly World world;
|
||||||
|
readonly PaletteFromPngInfo info;
|
||||||
|
public PaletteFromPng(World world, PaletteFromPngInfo info)
|
||||||
|
{
|
||||||
|
this.world = world;
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadPalettes(WorldRenderer wr)
|
||||||
|
{
|
||||||
|
if (info.Tileset != null && info.Tileset.ToLowerInvariant() != world.Map.Tileset.ToLowerInvariant())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var png = new Png(world.Map.Open(info.Filename));
|
||||||
|
var colors = new uint[Palette.Size];
|
||||||
|
|
||||||
|
for (var i = 0; i < png.Palette.Length; i++)
|
||||||
|
colors[i] = (uint)png.Palette[i].ToArgb();
|
||||||
|
|
||||||
|
wr.AddPalette(info.Name, new ImmutablePalette(colors), info.AllowModifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> PaletteNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Only expose the palette if it is available for the shellmap's tileset (which is a requirement for its use).
|
||||||
|
if (info.Tileset == null || info.Tileset == world.Map.Rules.TileSet.Id)
|
||||||
|
yield return info.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,14 +35,14 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
|||||||
{
|
{
|
||||||
var inputFiles = GlobArgs(args).OrderBy(a => a).ToList();
|
var inputFiles = GlobArgs(args).OrderBy(a => a).ToList();
|
||||||
var dest = inputFiles[0].Split('-').First() + ".shp";
|
var dest = inputFiles[0].Split('-').First() + ".shp";
|
||||||
var frames = inputFiles.Select(a => PngLoader.Load(a));
|
|
||||||
|
|
||||||
var size = frames.First().Size;
|
var frames = inputFiles.Select(a => new Png(File.OpenRead(a))).ToList();
|
||||||
if (frames.Any(f => f.Size != size))
|
var size = new Size(frames[0].Width, frames[0].Height);
|
||||||
|
if (frames.Any(f => f.Width != size.Width || f.Height != size.Height))
|
||||||
throw new InvalidOperationException("All frames must be the same size");
|
throw new InvalidOperationException("All frames must be the same size");
|
||||||
|
|
||||||
using (var destStream = File.Create(dest))
|
using (var destStream = File.Create(dest))
|
||||||
ShpTDSprite.Write(destStream, size, frames.Select(f => ToBytes(f)));
|
ShpTDSprite.Write(destStream, size, frames.Select(f => f.Data));
|
||||||
|
|
||||||
Console.WriteLine(dest + " saved.");
|
Console.WriteLine(dest + " saved.");
|
||||||
}
|
}
|
||||||
@@ -53,20 +53,5 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
|||||||
foreach (var path in Glob.Expand(args[i]))
|
foreach (var path in Glob.Expand(args[i]))
|
||||||
yield return path;
|
yield return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] ToBytes(Bitmap bitmap)
|
|
||||||
{
|
|
||||||
var data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly,
|
|
||||||
PixelFormat.Format8bppIndexed);
|
|
||||||
|
|
||||||
var bytes = new byte[bitmap.Width * bitmap.Height];
|
|
||||||
for (var i = 0; i < bitmap.Height; i++)
|
|
||||||
Marshal.Copy(new IntPtr(data.Scan0.ToInt64() + i * data.Stride),
|
|
||||||
bytes, i * bitmap.Width, bitmap.Width);
|
|
||||||
|
|
||||||
bitmap.UnlockBits(data);
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenRA.FileFormats;
|
||||||
|
|
||||||
|
namespace OpenRA.Mods.Common.UtilityCommands
|
||||||
|
{
|
||||||
|
public class PngSheetExportMetadataCommand : IUtilityCommand
|
||||||
|
{
|
||||||
|
string IUtilityCommand.Name { get { return "--png-sheet-export"; } }
|
||||||
|
|
||||||
|
bool IUtilityCommand.ValidateArguments(string[] args)
|
||||||
|
{
|
||||||
|
return args.Length == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Desc("PNGFILE", "Export png metadata to yaml")]
|
||||||
|
void IUtilityCommand.Run(Utility utility, string[] args)
|
||||||
|
{
|
||||||
|
using (var s = File.OpenRead(args[1]))
|
||||||
|
{
|
||||||
|
var png = new Png(s);
|
||||||
|
png.EmbeddedData.Select(m => new MiniYamlNode(m.Key, m.Value))
|
||||||
|
.ToList()
|
||||||
|
.WriteToFile(Path.ChangeExtension(args[1], "yaml"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#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.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using ICSharpCode.SharpZipLib.Checksums;
|
||||||
|
|
||||||
|
namespace OpenRA.Mods.Common.UtilityCommands
|
||||||
|
{
|
||||||
|
public class PngSheetImportMetadataCommand : IUtilityCommand
|
||||||
|
{
|
||||||
|
string IUtilityCommand.Name { get { return "--png-sheet-import"; } }
|
||||||
|
|
||||||
|
bool IUtilityCommand.ValidateArguments(string[] args)
|
||||||
|
{
|
||||||
|
return args.Length == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Desc("PNGFILE", "Import yaml metadata to png")]
|
||||||
|
void IUtilityCommand.Run(Utility utility, string[] args)
|
||||||
|
{
|
||||||
|
// HACK: This could eventually be merged into the Png class (add a Write method), however this requires complex png writing algorythms.
|
||||||
|
var rs = File.OpenRead(args[1]);
|
||||||
|
var ws = new MemoryStream();
|
||||||
|
|
||||||
|
using (var bw = new BinaryWriter(ws))
|
||||||
|
{
|
||||||
|
bw.Write(rs.ReadBytes(8));
|
||||||
|
var crc32 = new Crc32();
|
||||||
|
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
var length = IPAddress.NetworkToHostOrder(rs.ReadInt32());
|
||||||
|
var type = Encoding.UTF8.GetString(rs.ReadBytes(4));
|
||||||
|
var content = rs.ReadBytes(length);
|
||||||
|
var crc = rs.ReadUInt32();
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "tEXt":
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "IEND":
|
||||||
|
rs.Close();
|
||||||
|
|
||||||
|
foreach (var node in MiniYaml.FromFile(Path.ChangeExtension(args[1], "yaml")))
|
||||||
|
{
|
||||||
|
bw.Write(IPAddress.NetworkToHostOrder(node.Key.Length + 1 + node.Value.Value.Length));
|
||||||
|
bw.Write("tEXt".ToCharArray());
|
||||||
|
bw.Write(node.Key.ToCharArray());
|
||||||
|
bw.Write((byte)0x00);
|
||||||
|
bw.Write(node.Value.Value.ToCharArray());
|
||||||
|
crc32.Reset();
|
||||||
|
crc32.Update(Encoding.ASCII.GetBytes("tEXt"));
|
||||||
|
crc32.Update(Encoding.ASCII.GetBytes(node.Key + (char)0x00 + node.Value.Value));
|
||||||
|
bw.Write((uint)IPAddress.NetworkToHostOrder((int)crc32.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
bw.Write(0);
|
||||||
|
bw.Write(type.ToCharArray());
|
||||||
|
bw.Write(crc);
|
||||||
|
|
||||||
|
File.WriteAllBytes(args[1], ws.ToArray());
|
||||||
|
ws.Close();
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
bw.Write(IPAddress.NetworkToHostOrder(length));
|
||||||
|
bw.Write(type.ToCharArray());
|
||||||
|
bw.Write(content);
|
||||||
|
bw.Write(crc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user