2019-01-13 20:22:53 +00:00
using Newtonsoft.Json ;
2019-01-20 07:07:52 +00:00
using RecrownedAthenaeum.Data ;
2018-12-06 18:19:30 +00:00
using SixLabors.ImageSharp ;
using SixLabors.ImageSharp.PixelFormats ;
2018-12-08 03:48:02 +00:00
using SixLabors.ImageSharp.Processing ;
using SixLabors.Primitives ;
2019-01-13 20:22:53 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
2018-12-08 03:48:02 +00:00
using System.Linq ;
2019-01-13 19:55:08 +00:00
using System.Security ;
2018-12-06 18:19:30 +00:00
2018-12-07 08:22:15 +00:00
namespace RecrownedAthenaeum.Tools.TextureAtlas
2018-12-06 18:19:30 +00:00
{
2018-12-29 06:29:53 +00:00
public class TexturePacker
2018-12-06 18:19:30 +00:00
{
private enum SupportedExtensions
{
jpeg , jpg , png
}
2018-12-07 08:20:53 +00:00
int powLimit ;
2018-12-06 18:19:30 +00:00
Node masterNode ;
2019-04-11 05:34:49 +00:00
List < ImageHandler > imageHandlers ;
2018-12-29 07:10:25 +00:00
Dictionary < string , NinePatchData > ninePatchDictionary ;
2018-12-09 19:45:47 +00:00
int tpl ;
2018-12-29 06:29:53 +00:00
int TexturePowerLength { get { return tpl ; } set { TextureLength = ( int ) Math . Pow ( 2 , value ) ; tpl = value ; } }
public int TextureLength { get ; private set ; }
2019-04-11 05:34:49 +00:00
public int TexturesFound { get { return imageHandlers . Count ; } }
2018-12-07 08:20:53 +00:00
/// <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>
internal TexturePacker ( string rootDirectoryPath , int powLimit = 12 , int startingPower = 8 )
2018-12-06 18:19:30 +00:00
{
2018-12-07 08:20:53 +00:00
this . powLimit = powLimit ;
2018-12-29 07:10:25 +00:00
string [ ] paths ;
try
{
2019-01-13 19:55:08 +00:00
paths = Directory . GetFiles ( rootDirectoryPath ) ;
}
catch ( IOException )
2018-12-29 07:10:25 +00:00
{
2019-01-13 19:55:08 +00:00
throw new ArgumentException ( "Path " + rootDirectoryPath + " couldn't be resolved." ) ;
2018-12-29 07:10:25 +00:00
}
2018-12-09 19:45:47 +00:00
TexturePowerLength = startingPower ;
2018-12-07 08:20:53 +00:00
List < ImageHandler > imageHandlers = new List < ImageHandler > ( ) ;
2018-12-09 19:45:47 +00:00
int minAreaRequired = 0 ;
2018-12-06 18:19:30 +00:00
for ( int pathID = 0 ; pathID < paths . Length ; pathID + + )
{
SupportedExtensions extension ;
2019-04-11 05:34:49 +00:00
if ( Enum . TryParse ( Path . GetExtension ( paths [ pathID ] ) . ToLower ( ) . Substring ( 1 ) , out extension ) )
2018-12-06 18:19:30 +00:00
{
2019-04-11 05:34:49 +00:00
ConsoleUtilities . WriteWrappedLine ( "Reading texture data for: " + paths [ pathID ] ) ;
2018-12-07 08:20:53 +00:00
ImageHandler image = new ImageHandler ( paths [ pathID ] ) ;
imageHandlers . Add ( image ) ;
2018-12-09 19:45:47 +00:00
minAreaRequired + = image . Area ;
2018-12-29 07:10:25 +00:00
while ( minAreaRequired > TextureLength * TextureLength )
2018-12-09 19:45:47 +00:00
{
TexturePowerLength + + ;
}
2018-12-07 08:20:53 +00:00
}
2018-12-29 07:10:25 +00:00
else if ( Path . GetExtension ( paths [ pathID ] ) . ToLower ( ) = = ".9p" )
{
if ( ninePatchDictionary = = null ) ninePatchDictionary = new Dictionary < string , NinePatchData > ( ) ;
2019-01-13 19:55:08 +00:00
ConsoleUtilities . WriteWrappedLine ( "Reading ninepatch data for: " + paths [ pathID ] ) ;
string serialized = File . ReadAllText ( paths [ pathID ] ) ;
NinePatchData npData = JsonConvert . DeserializeObject < NinePatchData > ( serialized ) ;
ninePatchDictionary . Add ( npData . textureName , npData ) ;
2018-12-29 07:10:25 +00:00
}
2018-12-07 08:20:53 +00:00
}
imageHandlers . Sort ( ) ;
2019-04-11 05:34:49 +00:00
this . imageHandlers = imageHandlers ;
2018-12-07 08:20:53 +00:00
}
2018-12-06 18:19:30 +00:00
2018-12-07 08:20:53 +00:00
/// <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 ( ) ;
2018-12-29 06:29:53 +00:00
masterNode . region . Width = TextureLength ;
masterNode . region . Height = TextureLength ;
2019-04-11 05:34:49 +00:00
Queue < ImageHandler > imageHandlerQueue = new Queue < ImageHandler > ( imageHandlers ) ;
2018-12-07 08:20:53 +00:00
ImageHandler imageHandler ;
while ( imageHandlerQueue . TryDequeue ( out imageHandler ) )
{
2018-12-07 17:20:46 +00:00
Node activeNode = null ;
2018-12-09 19:45:47 +00:00
activeNode = masterNode . InsertImageHandler ( imageHandler ) ;
if ( activeNode = = null )
2018-12-07 08:20:53 +00:00
{
2018-12-09 19:45:47 +00:00
if ( ! AutoCorrectAtlasSize | | TexturePowerLength + 1 > powLimit )
2018-12-07 08:20:53 +00:00
{
2018-12-29 06:29:53 +00:00
throw new InvalidOperationException ( "Texture not large enough. Current size: " + TextureLength + "x" + TextureLength + "." ) ;
2018-12-06 18:19:30 +00:00
}
2018-12-09 19:45:47 +00:00
TexturePowerLength + = 1 ;
imageHandlerQueue . Clear ( ) ;
Build ( AutoCorrectAtlasSize ) ;
2018-12-06 18:19:30 +00:00
}
2019-04-11 05:34:49 +00:00
if ( ninePatchDictionary ! = null & & ninePatchDictionary . ContainsKey ( imageHandler . name ) )
2018-12-29 07:10:25 +00:00
{
2019-04-11 05:34:49 +00:00
NinePatchData npd = ninePatchDictionary [ imageHandler . name ] ;
imageHandler . ninePatchData = npd ;
if ( npd . textureName . Contains ( "-texture" ) )
{
imageHandler . name = imageHandler . name . Remove ( imageHandler . name . IndexOf ( "-texture" ) , 8 ) ;
}
2018-12-29 07:10:25 +00:00
}
2018-12-06 18:19:30 +00:00
}
2018-12-29 07:10:25 +00:00
2018-12-07 08:20:53 +00:00
}
/// <summary>
/// Renders the build into a PNG file and generates the respective <see cref="TextureAtlasData"/> meant for serialization and later to be loaded.
/// </summary>
2018-12-08 23:05:21 +00:00
/// <param name="output">directory to output to.</param>
/// <param name="atlasName">name of atlas.</param>
public void Save ( string output , string atlasName )
2018-12-07 08:20:53 +00:00
{
2018-12-08 03:48:02 +00:00
GraphicsOptions gOptions = new GraphicsOptions ( ) ;
2019-04-11 05:34:49 +00:00
TextureAtlasData . AtlasRegionData [ ] regions = new TextureAtlasData . AtlasRegionData [ TexturesFound ] ;
2018-12-06 18:19:30 +00:00
2018-12-29 06:29:53 +00:00
using ( Image < Rgba32 > atlasTexture = new Image < Rgba32 > ( TextureLength , TextureLength ) )
2018-12-08 03:48:02 +00:00
{
2019-04-11 05:34:49 +00:00
ImageHandler [ ] imageHandlers = this . imageHandlers . ToArray ( ) ;
2018-12-08 03:48:02 +00:00
for ( int i = 0 ; i < imageHandlers . Length ; i + + )
{
2019-01-15 05:38:50 +00:00
regions [ i ] = new TextureAtlasData . AtlasRegionData ( ) ;
2018-12-09 19:45:47 +00:00
ImageHandler ih = imageHandlers [ i ] ;
regions [ i ] . SetBounds ( ih . x , ih . y , ih . Width , ih . Height ) ;
regions [ i ] . ninePatchData = ih . ninePatchData ;
2019-04-11 05:34:49 +00:00
if ( regions [ i ] . ninePatchData ! = null ) regions [ i ] . ninePatchData . textureName = null ;
regions [ i ] . name = Path . GetFileNameWithoutExtension ( ih . name ) ;
2018-12-09 19:45:47 +00:00
using ( Image < Rgba32 > image = Image . Load < Rgba32 > ( ih . path ) )
{
atlasTexture . Mutate ( img = > img . DrawImage ( gOptions , image , new Point ( ih . x , ih . y ) ) ) ;
}
2018-12-08 03:48:02 +00:00
}
2018-12-09 19:45:47 +00:00
Directory . CreateDirectory ( output ) ;
2019-03-21 00:28:16 +00:00
using ( FileStream stream = new FileStream ( output + "/" + atlasName + "-texture" + ".png" , FileMode . Create ) )
2018-12-08 03:48:02 +00:00
{
atlasTexture . SaveAsPng ( stream ) ;
}
}
2019-03-21 00:28:16 +00:00
string serialized = JsonConvert . SerializeObject ( new TextureAtlasData ( atlasName + "-texture" + ".png" , regions ) , Formatting . Indented ) ;
2019-01-13 19:55:08 +00:00
File . WriteAllText ( output + "/" + atlasName + ".tatlas" , serialized ) ;
2018-12-08 03:48:02 +00:00
}
public void SetNinePatch ( string fileName , int a , int b , int c , int d )
{
NinePatchData ninePatchData = new NinePatchData ( fileName , a , b , c , d ) ;
2019-04-11 05:34:49 +00:00
RetrieveImageHandler ( fileName ) . ninePatchData = ninePatchData ;
}
public void RemoveNinePatch ( string name )
{
RetrieveImageHandler ( name ) . ninePatchData = null ;
2018-12-08 03:48:02 +00:00
}
2019-04-11 05:34:49 +00:00
private ImageHandler RetrieveImageHandler ( string name )
2018-12-08 03:48:02 +00:00
{
2019-04-11 05:34:49 +00:00
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 ) ;
2018-12-06 18:19:30 +00:00
}
private class Node
{
public Node parent ;
2018-12-07 08:20:53 +00:00
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 ;
2018-12-09 19:45:47 +00:00
public bool ContainsImage = false ;
public bool CanPlaceImage { get { return ( a = = null & & b = = null & & ! ContainsImage ) ; } }
2018-12-07 08:20:53 +00:00
public Node ( Node parent = null )
{
this . parent = parent ;
2018-12-09 19:45:47 +00:00
if ( parent ! = null ) region = parent . region ;
2018-12-07 08:20:53 +00:00
}
2018-12-06 18:19:30 +00:00
2018-12-07 17:20:46 +00:00
/// <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>
2018-12-07 08:20:53 +00:00
public Node InsertImageHandler ( ImageHandler imageHandler )
{
if ( imageHandler . Width ! = region . Width )
{
if ( imageHandler . Width < region . Width )
{
if ( a = = null )
{
childA . region . Width = imageHandler . Width ;
}
2018-12-09 19:45:47 +00:00
Node attemptedNode = null ;
if ( ! childA . ContainsImage & & imageHandler . Width < = childA . region . Width )
2018-12-07 08:20:53 +00:00
{
2018-12-09 19:45:47 +00:00
attemptedNode = childA . InsertImageHandler ( imageHandler ) ;
2018-12-07 08:20:53 +00:00
}
2018-12-09 19:45:47 +00:00
if ( attemptedNode = = null & & ! childB . ContainsImage )
2018-12-07 08:20:53 +00:00
{
2018-12-09 19:45:47 +00:00
childB . region . Width = region . Width - childA . region . Width ;
childB . region . X = childA . region . X + childA . region . Width ;
attemptedNode = childB . InsertImageHandler ( imageHandler ) ;
2018-12-07 08:20:53 +00:00
}
2018-12-09 19:45:47 +00:00
return attemptedNode ;
2018-12-07 08:20:53 +00:00
}
}
else if ( imageHandler . Height ! = region . Height )
{
if ( imageHandler . Height < region . Height )
{
if ( a = = null )
{
childA . region . Height = imageHandler . Height ;
}
2018-12-09 19:45:47 +00:00
Node attemptedNode = null ;
if ( ! childA . ContainsImage & & imageHandler . Height < = childA . region . Height )
2018-12-07 08:20:53 +00:00
{
2018-12-09 19:45:47 +00:00
attemptedNode = childA . InsertImageHandler ( imageHandler ) ;
2018-12-07 08:20:53 +00:00
}
2018-12-09 19:45:47 +00:00
if ( attemptedNode = = null & & ! childB . ContainsImage )
2018-12-07 08:20:53 +00:00
{
2018-12-09 19:45:47 +00:00
childB . region . Height = region . Height - childA . region . Height ;
childB . region . Y = childA . region . Y + childA . region . Height ;
attemptedNode = childB . InsertImageHandler ( imageHandler ) ;
2018-12-07 08:20:53 +00:00
}
2018-12-09 19:45:47 +00:00
return attemptedNode ;
2018-12-07 08:20:53 +00:00
}
}
2018-12-09 19:45:47 +00:00
else if ( CanPlaceImage )
2018-12-07 08:20:53 +00:00
{
imageHandler . x = region . X ;
imageHandler . y = region . Y ;
2018-12-09 19:45:47 +00:00
ContainsImage = true ;
2018-12-07 08:20:53 +00:00
return this ;
}
2018-12-07 17:20:46 +00:00
return null ;
2018-12-06 18:19:30 +00:00
}
}
2018-12-09 19:45:47 +00:00
private class ImageHandler : IComparable < ImageHandler >
2018-12-06 18:19:30 +00:00
{
public readonly string path ;
2018-12-09 19:45:47 +00:00
public readonly IImageInfo image ;
2018-12-07 08:20:53 +00:00
public int Area { get { return image . Width * image . Height ; } }
2019-04-11 05:34:49 +00:00
public string name ;
2018-12-07 08:20:53 +00:00
public int Width { get { return image . Width ; } }
public int Height { get { return image . Height ; } }
public int x , y ;
2018-12-08 03:48:02 +00:00
public NinePatchData ninePatchData ;
2018-12-06 18:19:30 +00:00
2019-04-11 05:34:49 +00:00
internal ImageHandler ( string path )
2018-12-06 18:19:30 +00:00
{
this . path = path ;
2019-04-11 05:34:49 +00:00
name = Path . GetFileName ( path ) ;
2019-01-13 19:55:08 +00:00
try
{
using ( FileStream stream = new FileStream ( path , FileMode . Open ) )
{
image = Image . Identify ( stream ) ;
}
} catch ( SecurityException )
2018-12-07 08:20:53 +00:00
{
2019-01-13 19:55:08 +00:00
throw new ArgumentException ( "Security exception occurred for image: " + path ) ;
2018-12-07 08:20:53 +00:00
}
}
public int CompareTo ( ImageHandler tImage )
{
return Area - tImage . Area ;
}
2018-12-06 18:19:30 +00:00
}
}
}