Skip to content

Commit

Permalink
Add ReadTags Only Method For FlatRedBall Integraiton (#25)
Browse files Browse the repository at this point in the history
* Adds ExtractCel method
ExtractCel methods allow consumers to extract the contents of a single cel into a texture object without having to flatten the entire frame.

* Remove invalid character

* Add example of using ExtractCel

* Bump version to 1.6.0

* Add method to read tag data only for FlatRedBall integration

* Bump version to 1.7.0
  • Loading branch information
AristurtleDev authored Mar 30, 2024
1 parent efe2cee commit 660368c
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<NeutralLanguage>en</NeutralLanguage>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.6.0</Version>
<Version>1.7.0</Version>
</PropertyGroup>

<!-- Setup Code Analysis using the .editorconfig file -->
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
A Cross Platform C# Library for Reading Aseprite Files

[![main](https://github.com/AristurtleDev/AsepriteDotNet/actions/workflows/main.yml/badge.svg)](https://github.com/AristurtleDev/AsepriteDotNet/actions/workflows/main.yml)
[![Nuget 1.6.0](https://img.shields.io/nuget/v/AsepriteDotNet?color=blue&style=flat-square)](https://www.nuget.org/packages/AsepriteDotNet/1.6.0)
[![Nuget 1.7.0](https://img.shields.io/nuget/v/AsepriteDotNet?color=blue&style=flat-square)](https://www.nuget.org/packages/AsepriteDotNet/1.7.0)
[![License: MIT](https://img.shields.io/badge/📃%20license-MIT-blue?style=flat)](LICENSE)
[![Twitter](https://img.shields.io/badge/%20-Share%20On%20Twitter-555?style=flat&logo=twitter)](https://twitter.com/intent/tweet?text=AsepriteDotNet%20by%20%40aristurtledev%0A%0AA%20new%20cross-platform%20library%20in%20C%23%20for%20reading%20Aseprite%20.ase%2F.aseprite%20files.%20https%3A%2F%2Fgithub.com%2FAristurtleDev%2FAsepriteDotNet%0A%0A%23aseprite%20%23dotnet%20%23csharp%20%23oss%0A)
</h1>
Expand All @@ -27,7 +27,7 @@ A Cross Platform C# Library for Reading Aseprite Files
# Installation
Install via NuGet
```
dotnet add package AsepriteDotNet --version 1.6.0
dotnet add package AsepriteDotNet --version 1.7.0
```

# Usage
Expand Down
159 changes: 159 additions & 0 deletions source/AsepriteDotNet/IO/AsepriteFileLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.
// See LICENSE file in the project root for full license information.

using System;
using AsepriteDotNet.Aseprite;
using AsepriteDotNet.Aseprite.Document;
using AsepriteDotNet.Aseprite.Types;
Expand Down Expand Up @@ -54,6 +55,164 @@ public static AsepriteFile FromStream(string fileName, Stream stream, bool leave
return LoadFile(fileName, reader, preMultiplyAlpha);
}

/// <summary>
/// Reads only the tag data from the Aseprite file.
/// </summary>
/// <param name="path">The absolute file path to the Aseprite file to load.</param>
/// <returns>An array of all tag data from the Aseprite file.</returns>
public static AsepriteTag[] ReadTags(string path)
{
using FileStream stream = File.OpenRead(path);
return ReadTags(stream);
}

/// <summary>
/// Reads only the tag data from the Aseprite file.
/// </summary>
/// <param name="stream">The stream to load the Aseprite file from.</param>
/// <param name="leaveOpen">
/// <see langword="true"/> to leave the given <paramref name="stream"/> open after loading the Aseprite file;
/// otherwise, <see langword="false"/>.
/// </param>
/// <returns>An array of all tag data from the Aseprite file.</returns>
public static AsepriteTag[] ReadTags(Stream stream, bool leaveOpen = false)
{
using AsepriteBinaryReader reader = new AsepriteBinaryReader(stream, leaveOpen);
return ReadTags(reader);
}

private static AsepriteTag[] ReadTags(AsepriteBinaryReader reader)
{
List<AsepriteTag> tags = new List<AsepriteTag>();

// Read the file header
AsepriteFileHeader fileHeader = reader.ReadUnsafe<AsepriteFileHeader>(AsepriteFileHeader.StructSize);

// Validate the file header magic number
if (fileHeader.MagicNumber != ASE_HEADER_MAGIC)
{
reader.Dispose();
throw new InvalidOperationException($"invalid file header magic number: 0x{fileHeader.MagicNumber:X4}. This does not appear to be a valid Aseprite file.");
}

// All tags exist within the first frame data so we only need to read one frame
AsepriteFrameHeader frameHeader = reader.ReadUnsafe<AsepriteFrameHeader>(AsepriteFrameHeader.StructSize);

// Validate the magic number in frame header
if (frameHeader.MagicNumber != ASE_FRAME_MAGIC)
{
throw new InvalidOperationException($"Frame 0 contains an invalid magic number: 0x{frameHeader.MagicNumber:X4}");
}

// Determine the number of chunks to read
int chunkCount = frameHeader.OldChunkCount;
if (chunkCount == 0xFFFF && chunkCount < frameHeader.NewChunkCount)
{
chunkCount = (int)frameHeader.NewChunkCount;
}

// Reference to the user data object to apply user data to from the last chunk that was read that
// could have had user data
AsepriteUserData? currentUserData = null;

// Tracks the iteration of the tags when reading user data for tags chunk.
int tagIterator = 0;

uint? lastReadChunkType = null;

// Read until we get the tags then exit early
for (int chunkNum = 0; chunkNum < chunkCount; chunkNum++)
{
long chunkStart = reader.Position;
AsepriteChunkHeader chunkHeader = reader.ReadUnsafe<AsepriteChunkHeader>(AsepriteChunkHeader.StructSize);
long chunkEnd = chunkStart + chunkHeader.ChunkSize;

switch (chunkHeader.ChunkType)
{
case ASE_CHUNK_TAGS:
{
ushort tagCount = reader.ReadWord();
reader.Ignore(8);

for (int i = 0; i < tagCount; i++)
{
AsepriteTagProperties properties = reader.ReadUnsafe<AsepriteTagProperties>(AsepriteTagProperties.StructSize);

// Validate loop direction
if (!Enum.IsDefined<AsepriteLoopDirection>((AsepriteLoopDirection)properties.Direction))
{
reader.Dispose();
throw new InvalidOperationException($"Unknown loop direction: {properties.Direction}");
}

string tagName = reader.ReadString(properties.NameLen);

AsepriteTag tag = new AsepriteTag(properties, tagName);
currentUserData = tag.UserData;
lastReadChunkType = chunkHeader.ChunkType;
tags.Add(tag);
}
}
break;

case ASE_CHUNK_USER_DATA:
{
uint flags = reader.ReadDword();
string? text = null;
Rgba32? color = null;

if (Calc.HasFlag(flags, ASE_USER_DATA_FLAG_HAS_TEXT))
{
text = reader.ReadString();
}

if (Calc.HasFlag(flags, ASE_USER_DATA_FLAG_HAS_COLOR))
{
color = reader.ReadUnsafe<Rgba32>(Rgba32.StructSize);
}

if (currentUserData is not null)
{
currentUserData.Text = text;
currentUserData.Color = color;

if (lastReadChunkType == ASE_CHUNK_TAGS)
{
// Tags are a special case. User data for tags comes all together
// (one next to the other) after the tags chunk, in the same order:
//
// TAGS CHUNK (TAG1, TAG2, ..., TAGn)
// USER DATA CHUNK FOR TAG1
// USER DATA CHUNK FOR TAG2
// ...
// USER DATA CHUNK FOR TAGn
//
// So here we expect that the next user data chunk will correspond to the next tag
// int he tags collection
tagIterator++;

if (tagIterator < tags.Count)
{
currentUserData = tags[tagIterator].UserData;
}
else
{
currentUserData = null;
lastReadChunkType = null;
}
}
}
}
break;

}
reader.Seek(chunkEnd, SeekOrigin.Begin);
}

return tags.ToArray();
}


private static AsepriteFile LoadFile(string fileName, AsepriteBinaryReader reader, bool preMultiplyAlpha)
{
// Collection of non-fatal warnings accumulated while loading the Aseprite file. Provided to the consumer as a
Expand Down
21 changes: 21 additions & 0 deletions tests/AsepriteDotNet.Tests/IO/AsepriteFileLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,26 @@ public void AsepriteFileReader_SpriteUserDataTest()
Assert.Equal("Test Sprite UserData", doc.UserData.Text);
Assert.Equal(new Rgba32(1, 2, 3, 4), doc.UserData.Color);
}

[Fact]
public void AsepriteFileReader_ReadTagsTest()
{
string path = GetPath("read-test.aseprite");
AsepriteTag[] tags = AsepriteFileLoader.ReadTags(path);

Assert.Equal(3, tags.Length);
Assert.Equal("tag0to2forward", tags[0].Name);
Assert.Equal(0, tags[0].From);
Assert.Equal(2, tags[0].To);
Assert.Equal(new Rgba32(0, 0, 0, 255), tags[0].Color);
Assert.Equal(AsepriteLoopDirection.Forward, tags[0].LoopDirection);
Assert.Equal("tag3pingpong", tags[1].Name);
Assert.Equal(AsepriteLoopDirection.PingPong, tags[1].LoopDirection);
Assert.Equal("tag4userdata", tags[2].Name);
Assert.Equal(new Rgba32(11, 255, 230, 255), tags[2].Color);
Assert.Equal(new Rgba32(11, 255, 230, 255), tags[2].UserData.Color);
Assert.Equal("tag-4-user-data", tags[2].UserData.Text);

}
}
}

0 comments on commit 660368c

Please sign in to comment.