2020-02-17 02:44:21 +00:00
using Newtonsoft.Json ;
2020-04-18 03:09:31 +00:00
using RecrownedGTK.AssetsSystem.Information ;
2020-02-17 02:44:21 +00:00
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.Security ;
2020-02-24 01:51:39 +00:00
using RecrownedGTK.Tools.CommandProcessor ;
2020-02-17 02:44:21 +00:00
namespace RecrownedGTK.Tools.TextureAtlas
{
public class TexturePacker
{
private enum SupportedExtensions
{
jpeg , jpg , png
}
int powLimit ;
Node masterNode ;
List < ImageHandler > imageHandlers ;
2020-02-23 19:50:01 +00:00
Dictionary < string , NinePatchInfo > ninePatchDictionary ;
2020-02-17 02:44:21 +00:00
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 ; } }
/// <summary>
/// Machine to pack multiple textures into one large texture.
/// </summary>
/// <param name="rootDirectoryPath">Path to textures.</param>
/// <param name="powLimit">Power of two limit for auto expanding texture. Default is 12 which is a 4096x4096 texture.</param>
/// <param name="startingPower">What power to start at and build up from. Default is 8 which is a 256x256 texture.</param>
2020-02-24 01:51:39 +00:00
internal TexturePacker ( IUserOutput userOutput , string rootDirectoryPath , int powLimit = 12 , int startingPower = 8 )
2020-02-17 02:44:21 +00:00
{
this . powLimit = powLimit ;
string [ ] paths ;
try
{
paths = Directory . GetFiles ( rootDirectoryPath ) ;
}
catch ( IOException )
{
throw new ArgumentException ( "Path " + rootDirectoryPath + " couldn't be resolved." ) ;
}
TexturePowerLength = startingPower ;
List < ImageHandler > imageHandlers = new List < ImageHandler > ( ) ;
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 ) )
{
2020-02-24 01:51:39 +00:00
userOutput . WrappedOutput ( "Reading texture data for: " + paths [ pathID ] ) ;
2020-02-17 02:44:21 +00:00
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" )
{
2020-02-23 19:50:01 +00:00
if ( ninePatchDictionary = = null ) ninePatchDictionary = new Dictionary < string , NinePatchInfo > ( ) ;
2020-02-24 01:51:39 +00:00
userOutput . WrappedOutput ( "Reading ninepatch data for: " + paths [ pathID ] ) ;
2020-02-17 02:44:21 +00:00
string serialized = File . ReadAllText ( paths [ pathID ] ) ;
2020-02-23 19:50:01 +00:00
NinePatchInfo npData = JsonConvert . DeserializeObject < NinePatchInfo > ( serialized ) ;
ninePatchDictionary . Add ( npData . name , npData ) ;
2020-02-17 02:44:21 +00:00
}
}
imageHandlers . Sort ( ) ;
this . imageHandlers = imageHandlers ;
}
/// <summary>
/// Builds a texture atlas.
/// </summary>
/// <param name="AutoCorrectAtlasSize">Whether or not to automatically upscale atlas' texture in the case it is too small. Goes up to 4096 by default.</param>
public void Build ( bool AutoCorrectAtlasSize = true )
{
masterNode = new Node ( ) ;
masterNode . region . Width = TextureLength ;
masterNode . region . Height = TextureLength ;
Queue < ImageHandler > imageHandlerQueue = new Queue < ImageHandler > ( imageHandlers ) ;
ImageHandler imageHandler ;
2020-02-29 06:53:06 +00:00
while ( imageHandlerQueue . Count ! = 0 )
2020-02-17 02:44:21 +00:00
{
2020-02-29 06:53:06 +00:00
imageHandler = imageHandlerQueue . Dequeue ( ) ;
2020-02-17 02:44:21 +00:00
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 ) )
{
2020-02-23 19:50:01 +00:00
NinePatchInfo npd = ninePatchDictionary [ imageHandler . name ] ;
2020-02-17 02:44:21 +00:00
imageHandler . ninePatchData = npd ;
2020-02-23 19:50:01 +00:00
if ( npd . name . Contains ( "-texture" ) )
2020-02-17 02:44:21 +00:00
{
imageHandler . name = imageHandler . name . Remove ( imageHandler . name . IndexOf ( "-texture" ) , 8 ) ;
}
}
}
}
/// <summary>
/// Renders the build into a PNG file and generates the respective <see cref="TextureAtlasData"/> meant for serialization and later to be loaded.
/// </summary>
/// <param name="output">directory to output to.</param>
/// <param name="atlasName">name of atlas.</param>
public void Save ( string output , string atlasName )
{
GraphicsOptions gOptions = new GraphicsOptions ( ) ;
2020-02-23 19:50:01 +00:00
TextureMapInfo . MapRegionInfo [ ] regions = new TextureMapInfo . MapRegionInfo [ TexturesFound ] ;
2020-02-17 02:44:21 +00:00
using ( Image < Rgba32 > atlasTexture = new Image < Rgba32 > ( TextureLength , TextureLength ) )
{
ImageHandler [ ] imageHandlers = this . imageHandlers . ToArray ( ) ;
for ( int i = 0 ; i < imageHandlers . Length ; i + + )
{
ImageHandler ih = imageHandlers [ i ] ;
2020-02-23 19:50:01 +00:00
regions [ i ] = new TextureMapInfo . MapRegionInfo ( ) ;
2020-02-17 02:44:21 +00:00
regions [ i ] . SetBounds ( ih . x , ih . y , ih . Width , ih . Height ) ;
2020-02-23 19:50:01 +00:00
regions [ i ] . ninePatchInfo = ih . ninePatchData ;
if ( ! regions [ i ] . ninePatchInfo . Equals ( default ( NinePatchInfo ) ) ) regions [ i ] . ninePatchInfo . name = null ;
2020-02-17 02:44:21 +00:00
regions [ i ] . name = Path . GetFileNameWithoutExtension ( ih . name ) ;
using ( Image < Rgba32 > image = Image . Load < Rgba32 > ( 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 ) ;
}
}
2020-02-23 19:50:01 +00:00
string serialized = JsonConvert . SerializeObject ( new TextureMapInfo ( atlasName + "-texture" + ".png" , regions ) , Formatting . Indented ) ;
2020-02-17 02:44:21 +00:00
File . WriteAllText ( output + "/" + atlasName + ".tatlas" , serialized ) ;
}
public void SetNinePatch ( string fileName , int a , int b , int c , int d )
{
2020-02-23 19:50:01 +00:00
NinePatchInfo ninePatchData = new NinePatchInfo ( fileName , a , b , c , d ) ;
2020-02-17 02:44:21 +00:00
RetrieveImageHandler ( fileName ) . ninePatchData = ninePatchData ;
}
public void RemoveNinePatch ( string name )
{
2020-02-23 19:50:01 +00:00
RetrieveImageHandler ( name ) . ninePatchData = default ( NinePatchInfo ) ;
2020-02-17 02:44:21 +00:00
}
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 ;
}
/// <summary>
/// Attempts to insert image within the node. This builds the node to have children if needed.
/// </summary>
/// <param name="imageHandler">the image to insert.</param>
/// <returns>The node the image is placed in.</returns>
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 < ImageHandler >
{
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 ;
2020-02-23 19:50:01 +00:00
public NinePatchInfo ninePatchData ;
2020-02-17 02:44:21 +00:00
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 ;
}
}
}
}