using Newtonsoft.Json; using RecrownedAthenaeum.Data; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.Primitives; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security; namespace RecrownedAthenaeum.Tools.TextureAtlas { public class TexturePacker { private enum SupportedExtensions { jpeg, jpg, png } int powLimit; Node masterNode; List imageHandlers; Dictionary ninePatchDictionary; int tpl; int TexturePowerLength { get { return tpl; } set { TextureLength = (int)Math.Pow(2, value); tpl = value; } } public int TextureLength { get; private set; } public int TexturesFound { get { return imageHandlers.Count; } } /// /// Machine to pack multiple textures into one large texture. /// /// Path to textures. /// Power of two limit for auto expanding texture. Default is 12 which is a 4096x4096 texture. /// What power to start at and build up from. Default is 8 which is a 256x256 texture. internal TexturePacker(string rootDirectoryPath, int powLimit = 12, int startingPower = 8) { this.powLimit = powLimit; string[] paths; try { paths = Directory.GetFiles(rootDirectoryPath); } catch (IOException) { throw new ArgumentException("Path " + rootDirectoryPath + " couldn't be resolved."); } TexturePowerLength = startingPower; List imageHandlers = new List(); int minAreaRequired = 0; for (int pathID = 0; pathID < paths.Length; pathID++) { SupportedExtensions extension; if (Enum.TryParse(Path.GetExtension(paths[pathID]).ToLower().Substring(1), out extension)) { ConsoleUtilities.WriteWrappedLine("Reading texture data for: " + paths[pathID]); ImageHandler image = new ImageHandler(paths[pathID]); imageHandlers.Add(image); minAreaRequired += image.Area; while (minAreaRequired > TextureLength * TextureLength) { TexturePowerLength++; } } else if (Path.GetExtension(paths[pathID]).ToLower() == ".9p") { if (ninePatchDictionary == null) ninePatchDictionary = new Dictionary(); ConsoleUtilities.WriteWrappedLine("Reading ninepatch data for: " + paths[pathID]); string serialized = File.ReadAllText(paths[pathID]); NinePatchData npData = JsonConvert.DeserializeObject(serialized); ninePatchDictionary.Add(npData.textureName, npData); } } imageHandlers.Sort(); this.imageHandlers = imageHandlers; } /// /// Builds a texture atlas. /// /// Whether or not to automatically upscale atlas' texture in the case it is too small. Goes up to 4096 by default. public void Build(bool AutoCorrectAtlasSize = true) { masterNode = new Node(); masterNode.region.Width = TextureLength; masterNode.region.Height = TextureLength; Queue imageHandlerQueue = new Queue(imageHandlers); ImageHandler imageHandler; while (imageHandlerQueue.TryDequeue(out imageHandler)) { Node activeNode = null; activeNode = masterNode.InsertImageHandler(imageHandler); if (activeNode == null) { if (!AutoCorrectAtlasSize || TexturePowerLength + 1 > powLimit) { throw new InvalidOperationException("Texture not large enough. Current size: " + TextureLength + "x" + TextureLength + "."); } TexturePowerLength += 1; imageHandlerQueue.Clear(); Build(AutoCorrectAtlasSize); } if (ninePatchDictionary != null && ninePatchDictionary.ContainsKey(imageHandler.name)) { NinePatchData npd = ninePatchDictionary[imageHandler.name]; imageHandler.ninePatchData = npd; if (npd.textureName.Contains("-texture")) { imageHandler.name = imageHandler.name.Remove(imageHandler.name.IndexOf("-texture"), 8); } } } } /// /// Renders the build into a PNG file and generates the respective meant for serialization and later to be loaded. /// /// directory to output to. /// name of atlas. public void Save(string output, string atlasName) { GraphicsOptions gOptions = new GraphicsOptions(); TextureAtlasData.AtlasRegionData[] regions = new TextureAtlasData.AtlasRegionData[TexturesFound]; using (Image atlasTexture = new Image(TextureLength, TextureLength)) { ImageHandler[] imageHandlers = this.imageHandlers.ToArray(); for (int i = 0; i < imageHandlers.Length; i++) { regions[i] = new TextureAtlasData.AtlasRegionData(); ImageHandler ih = imageHandlers[i]; regions[i].SetBounds(ih.x, ih.y, ih.Width, ih.Height); regions[i].ninePatchData = ih.ninePatchData; if (regions[i].ninePatchData != null) regions[i].ninePatchData.textureName = null; regions[i].name = Path.GetFileNameWithoutExtension(ih.name); using (Image image = Image.Load(ih.path)) { atlasTexture.Mutate(img => img.DrawImage(gOptions, image, new Point(ih.x, ih.y))); } } Directory.CreateDirectory(output); using (FileStream stream = new FileStream(output + "/" + atlasName + "-texture" + ".png", FileMode.Create)) { atlasTexture.SaveAsPng(stream); } } string serialized = JsonConvert.SerializeObject(new TextureAtlasData(atlasName + "-texture" + ".png", regions), Formatting.Indented); File.WriteAllText(output + "/" + atlasName + ".tatlas", serialized); } public void SetNinePatch(string fileName, int a, int b, int c, int d) { NinePatchData ninePatchData = new NinePatchData(fileName, a, b, c, d); RetrieveImageHandler(fileName).ninePatchData = ninePatchData; } public void RemoveNinePatch(string name) { RetrieveImageHandler(name).ninePatchData = null; } private ImageHandler RetrieveImageHandler(string name) { for (int i = 0; i < TexturesFound; i++) { if (imageHandlers[i].name == name) { return imageHandlers[i]; } } throw new ArgumentException("Couldn't find texture with name: " + name); } private class Node { public Node parent; private Node a, b; public Node childA { get { if (a == null) a = new Node(this); return a; } set { value.parent = this; a = value; } } public Node childB { get { if (b == null) { b = new Node(this); } return b; } set { value.parent = this; b = value; } } public Rectangle region; public bool ContainsImage = false; public bool CanPlaceImage { get { return (a == null && b == null && !ContainsImage); } } public Node(Node parent = null) { this.parent = parent; if (parent != null) region = parent.region; } /// /// Attempts to insert image within the node. This builds the node to have children if needed. /// /// the image to insert. /// The node the image is placed in. public Node InsertImageHandler(ImageHandler imageHandler) { if (imageHandler.Width != region.Width) { if (imageHandler.Width < region.Width) { if (a == null) { childA.region.Width = imageHandler.Width; } Node attemptedNode = null; if (!childA.ContainsImage && imageHandler.Width <= childA.region.Width) { attemptedNode = childA.InsertImageHandler(imageHandler); } if (attemptedNode == null && !childB.ContainsImage) { childB.region.Width = region.Width - childA.region.Width; childB.region.X = childA.region.X + childA.region.Width; attemptedNode = childB.InsertImageHandler(imageHandler); } return attemptedNode; } } else if (imageHandler.Height != region.Height) { if (imageHandler.Height < region.Height) { if (a == null) { childA.region.Height = imageHandler.Height; } Node attemptedNode = null; if (!childA.ContainsImage && imageHandler.Height <= childA.region.Height) { attemptedNode = childA.InsertImageHandler(imageHandler); } if (attemptedNode == null && !childB.ContainsImage) { childB.region.Height = region.Height - childA.region.Height; childB.region.Y = childA.region.Y + childA.region.Height; attemptedNode = childB.InsertImageHandler(imageHandler); } return attemptedNode; } } else if (CanPlaceImage) { imageHandler.x = region.X; imageHandler.y = region.Y; ContainsImage = true; return this; } return null; } } private class ImageHandler : IComparable { public readonly string path; public readonly IImageInfo image; public int Area { get { return image.Width * image.Height; } } public string name; public int Width { get { return image.Width; } } public int Height { get { return image.Height; } } public int x, y; public NinePatchData ninePatchData; internal ImageHandler(string path) { this.path = path; name = Path.GetFileName(path); try { using (FileStream stream = new FileStream(path, FileMode.Open)) { image = Image.Identify(stream); } } catch (SecurityException) { throw new ArgumentException("Security exception occurred for image: " + path); } } public int CompareTo(ImageHandler tImage) { return Area - tImage.Area; } } } }