diff --git a/OpenRA.Game/FileFormats/PngLoader.cs b/OpenRA.Game/FileFormats/PngLoader.cs deleted file mode 100644 index 8f271fea91..0000000000 --- a/OpenRA.Game/FileFormats/PngLoader.cs +++ /dev/null @@ -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(); - - 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"); - } - } -} diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 728eef6783..a304b9b99b 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -273,7 +273,6 @@ - diff --git a/OpenRA.Mods.Common/FileFormats/Png.cs b/OpenRA.Mods.Common/FileFormats/Png.cs new file mode 100644 index 0000000000..8907937758 --- /dev/null +++ b/OpenRA.Mods.Common/FileFormats/Png.cs @@ -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 EmbeddedData = new Dictionary(); + + public Png(Stream s) + { + if (!Verify(s)) + throw new InvalidDataException("PNG Signature is bogus"); + + s.Position += 8; + var headerParsed = false; + + var data = new List(); + + 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"); + } + } +} diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index daeca3c8cc..b412dd55c5 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -146,6 +146,7 @@ + @@ -559,6 +560,7 @@ + @@ -596,6 +598,8 @@ + + @@ -697,6 +701,7 @@ + diff --git a/OpenRA.Mods.Common/SpriteLoaders/PngSheetLoader.cs b/OpenRA.Mods.Common/SpriteLoaders/PngSheetLoader.cs new file mode 100644 index 0000000000..786fa645f3 --- /dev/null +++ b/OpenRA.Mods.Common/SpriteLoaders/PngSheetLoader.cs @@ -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 frameRegions; + List 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 regions, out List offsets) + { + regions = new List(); + offsets = new List(); + + 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("Region", coords[0])); + offsets.Add(FieldLoader.GetValue("Offset", coords[1])); + } + } + + void RegionsFromSlices(Png png, out List regions, out List 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("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("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("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("Offset", png.EmbeddedData["Offset"]); + else + offset = float2.Zero; + + var framesPerRow = png.Width / frameSize.Width; + + regions = new List(); + offsets = new List(); + + 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); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/World/PaletteFromPng.cs b/OpenRA.Mods.Common/Traits/World/PaletteFromPng.cs new file mode 100644 index 0000000000..74c9e6371b --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/PaletteFromPng.cs @@ -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 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; + } + } + } +} diff --git a/OpenRA.Mods.Common/UtilityCommands/ConvertPngToShpCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ConvertPngToShpCommand.cs index e54afca572..556ea2cd33 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ConvertPngToShpCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ConvertPngToShpCommand.cs @@ -35,14 +35,14 @@ namespace OpenRA.Mods.Common.UtilityCommands { var inputFiles = GlobArgs(args).OrderBy(a => a).ToList(); var dest = inputFiles[0].Split('-').First() + ".shp"; - var frames = inputFiles.Select(a => PngLoader.Load(a)); - var size = frames.First().Size; - if (frames.Any(f => f.Size != size)) + var frames = inputFiles.Select(a => new Png(File.OpenRead(a))).ToList(); + 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"); 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."); } @@ -53,20 +53,5 @@ namespace OpenRA.Mods.Common.UtilityCommands foreach (var path in Glob.Expand(args[i])) 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; - } } } diff --git a/OpenRA.Mods.Common/UtilityCommands/PngSheetExportMetadataCommand.cs b/OpenRA.Mods.Common/UtilityCommands/PngSheetExportMetadataCommand.cs new file mode 100644 index 0000000000..5991125849 --- /dev/null +++ b/OpenRA.Mods.Common/UtilityCommands/PngSheetExportMetadataCommand.cs @@ -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")); + } + } + } +} diff --git a/OpenRA.Mods.Common/UtilityCommands/PngSheetImportMetadataCommand.cs b/OpenRA.Mods.Common/UtilityCommands/PngSheetImportMetadataCommand.cs new file mode 100644 index 0000000000..41c704787b --- /dev/null +++ b/OpenRA.Mods.Common/UtilityCommands/PngSheetImportMetadataCommand.cs @@ -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; + } + } + } + } + } +}