1
0
mirror of synced 2024-12-05 04:17:59 +01:00
Switch-Toolbox/File_Format_Library/FileFormats/Font/BFFNT.cs

1356 lines
46 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Toolbox.Library;
using System.IO;
using Toolbox.Library.IO;
using Toolbox.Library.Forms;
using System.Drawing;
using FirstPlugin.Forms;
using LibEveryFileExplorer.GFX;
using System.Drawing.Imaging;
namespace FirstPlugin
{
public class BFFNT : BXFNT, IFileFormat, IEditor<BffntEditor>, IConvertableTextFormat
{
public FileType FileType { get; set; } = FileType.Font;
public bool CanSave { get; set; }
public string[] Description { get; set; } = new string[] { "Cafe Font" };
public string[] Extension { get; set; } = new string[] { "*.bffnt" };
public string FileName { get; set; }
public string FilePath { get; set; }
public IFileInfo IFileInfo { get; set; }
public bool Identify(Stream stream)
{
using (var reader = new FileReader(stream, true))
{
return reader.CheckSignature(4, "FFNT");
}
}
public Type[] Types
{
get
{
List<Type> types = new List<Type>();
return types.ToArray();
}
}
public FFNT bffnt;
public BffntEditor OpenForm()
{
BffntEditor form = new BffntEditor();
form.Text = "BFFNT Editor";
form.Dock = DockStyle.Fill;
return form;
}
public void FillEditor(UserControl control)
{
((BffntEditor)control).LoadFontFile(this);
}
#region Text Converter Interface
public TextFileType TextFileType => TextFileType.Xml;
public bool CanConvertBack => false;
public string ConvertToString()
{
return BffntCharSet2Xlor.ToXlor(this);
}
public void ConvertFromString(string text)
{
}
#endregion
public void Load(System.IO.Stream stream)
{
PluginRuntime.BxfntFiles.Add(this);
CanSave = true;
bffnt = new FFNT();
bffnt.Read(new FileReader(stream));
TGLP tglp = bffnt.FontSection.TextureGlyph;
if (tglp.SheetDataList.Count > 0)
{
var bntx = STFileLoader.OpenFileFormat(
new MemoryStream(Utils.CombineByteArray(tglp.SheetDataList.ToArray())), "Sheet_0");
if (bntx != null)
{
tglp.BinaryTextureFile = (BNTX)bntx;
}
else
{
for (int s = 0; s < tglp.SheetDataList.Count; s++)
{
var surface = new Gx2ImageBlock();
surface.Text = $"Sheet_{s}";
surface.Load(tglp, s);
tglp.Gx2Textures.Add(surface);
}
}
}
int i = 0;
foreach (byte[] texture in tglp.SheetDataList)
{
// BNTX file = (BNTX)STFileLoader.OpenFileFormat("Sheet" + i++, texture);
// Nodes.Add(file);
}
}
public override string Name { get { return FileName; } }
public override BitmapFont GetBitmapFont(bool UseChannelComp = false)
{
return bffnt.GetBitmapFont(UseChannelComp);
}
public override Bitmap GetBitmap(string text, bool reversewh, LayoutBXLYT.BasePane pane)
{
return bffnt.GetBitmap(text, reversewh, pane);
}
public void Unload()
{
PluginRuntime.BxfntFiles.Remove(this);
}
public void Save(System.IO.Stream stream)
{
bffnt.Write(new FileWriter(stream));
}
public class SheetEntry : TreeNodeCustom
{
public SheetEntry()
{
ImageKey = "fileBlank";
SelectedImageKey = "fileBlank";
ContextMenu = new ContextMenu();
MenuItem export = new MenuItem("Export");
ContextMenu.MenuItems.Add(export);
export.Click += Export;
}
public byte[] data;
public override void OnClick(TreeView treeview)
{
}
private void Export(object sender, EventArgs args)
{
SaveFileDialog sfd = new SaveFileDialog();
sfd.FileName = Text;
sfd.DefaultExt = "bntx";
sfd.Filter = "Supported Formats|*.bntx;|" +
"All files(*.*)|*.*";
if (sfd.ShowDialog() == DialogResult.OK)
{
File.WriteAllBytes(sfd.FileName, data);
}
}
}
}
public class FFNT
{
public ushort BOM;
public ushort HeaderSize;
public uint Version { get; set; }
public FINF FontSection { get; set; }
public FontKerningTable KerningTable { get; set; }
public List<BFFNT_Block> Blocks = new List<BFFNT_Block>();
public PlatformType Platform { get; set; } = PlatformType.Cafe;
public enum PlatformType
{
Cafe,
NX,
Ctr
}
public void Read(FileReader reader)
{
reader.ByteOrder = Syroot.BinaryData.ByteOrder.BigEndian;
string Signature = reader.ReadString(4, Encoding.ASCII);
if (Signature != "FFNT" && Signature != "CFNT")
throw new Exception($"Invalid signature {Signature}! Expected FFNT or CFNT.");
BOM = reader.ReadUInt16();
reader.CheckByteOrderMark(BOM);
HeaderSize = reader.ReadUInt16();
Version = reader.ReadUInt32();
uint FileSize = reader.ReadUInt16();
ushort BlockCount = reader.ReadUInt16();
ushort Padding = reader.ReadUInt16();
if (reader.ByteOrder == Syroot.BinaryData.ByteOrder.LittleEndian)
{
if (Version > 0x3000000 || Version > 0x00000103)
Platform = PlatformType.NX;
else
Platform = PlatformType.Ctr;
}
else
Platform = PlatformType.Cafe;
if (Signature == "CFNT")
Platform = PlatformType.Ctr;
reader.Seek(HeaderSize, SeekOrigin.Begin);
FontSection = new FINF();
FontSection.Read(reader, this);
Blocks.Add(FontSection);
//Check for any unread blocks
reader.Seek(HeaderSize, SeekOrigin.Begin);
while (!reader.EndOfStream)
{
long BlockStart = reader.Position;
string BlockSignature = reader.ReadString(4, Encoding.ASCII);
uint BlockSize = reader.ReadUInt32();
switch (BlockSignature)
{
case "FFNT":
case "FFNA":
case "FCPX":
case "CWDH":
case "CGLP":
case "CMAP":
case "TGLP":
case "FINF":
break;
case "KRNG":
KerningTable = new FontKerningTable();
KerningTable.Read(reader, this);
break;
case "GLGR":
case "HTGL":
break;
default:
throw new Exception("Unknown block found! " + BlockSignature);
}
reader.SeekBegin(BlockStart + BlockSize);
}
}
internal int BlockCounter = 0;
public void Write(FileWriter writer)
{
writer.ByteOrder = Syroot.BinaryData.ByteOrder.BigEndian;
BlockCounter = 1;
writer.WriteSignature("FFNT");
writer.Write(BOM);
writer.CheckByteOrderMark(BOM);
writer.Write(HeaderSize);
writer.Write(Version);
long _ofsFileSize = writer.Position;
writer.Write(uint.MaxValue);
long _ofsBlockNum = writer.Position;
writer.Write((ushort)0); //BlockCount
writer.Write((ushort)0);
writer.SeekBegin(HeaderSize);
FontSection.Write(writer, this);
//Save Block Count
using (writer.TemporarySeek(_ofsBlockNum, SeekOrigin.Begin))
{
writer.Write((ushort)(BlockCounter + 1));
}
//Save File size
using (writer.TemporarySeek(_ofsFileSize, SeekOrigin.Begin))
{
writer.Write((uint)(writer.BaseStream.Length));
}
}
private string CheckSignature(FileReader reader)
{
string Signature = reader.ReadString(4, Encoding.ASCII);
reader.Seek(-4, SeekOrigin.Current);
return Signature;
}
private BitmapFont bitmapFont;
public Bitmap GetBitmap(string text, bool reversewh, LayoutBXLYT.BasePane pane)
{
var FontInfo = FontSection;
var TextureGlyph = FontInfo.TextureGlyph;
var textPane = (LayoutBXLYT.ITextPane)pane;
if (bitmapFont == null)
bitmapFont = GetBitmapFont(true);
return bitmapFont.PrintToBitmap(text, new BitmapFont.FontRenderSettings()
{
CharSpacing = (int)textPane.CharacterSpace,
XScale = (textPane.FontSize.X / TextureGlyph.CellWidth),
YScale = (textPane.FontSize.Y / TextureGlyph.CellHeight),
LineSpacing = (int)textPane.LineSpace,
});
}
public BitmapFont GetBitmapFont(bool UseChannelComp = false)
{
var FontInfo = FontSection;
var TextureGlyph = FontInfo.TextureGlyph;
BitmapFont f = new BitmapFont();
f.LineHeight = FontInfo.LineFeed;
Bitmap[] Chars = new Bitmap[TextureGlyph.LinesCount * TextureGlyph.RowCount * TextureGlyph.SheetCount];
float realcellwidth = TextureGlyph.CellWidth + 1;
float realcellheight = TextureGlyph.CellHeight + 1;
int j = 0;
for (int sheet = 0; sheet < TextureGlyph.SheetCount; sheet++)
{
Bitmap SheetBM = TextureGlyph.GetImageSheet(sheet).GetBitmap();
if (UseChannelComp)
SheetBM = TextureGlyph.GetImageSheet(sheet).GetComponentBitmap(SheetBM, true);
SheetBM.RotateFlip(RotateFlipType.RotateNoneFlipY);
BitmapData bd = SheetBM.LockBits(new Rectangle(0, 0, SheetBM.Width, SheetBM.Height),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
for (int y = 0; y < TextureGlyph.LinesCount; y++)
{
for (int x = 0; x < TextureGlyph.RowCount; x++)
{
Bitmap b = new Bitmap(TextureGlyph.CellWidth, TextureGlyph.CellHeight);
BitmapData bd2 = b.LockBits(new Rectangle(0, 0, b.Width, b.Height),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
for (int y2 = 0; y2 < TextureGlyph.CellHeight; y2++)
{
for (int x2 = 0; x2 < TextureGlyph.CellWidth; x2++)
{
Marshal.WriteInt32(bd2.Scan0, y2 * bd2.Stride + x2 * 4,
Marshal.ReadInt32(bd.Scan0, (int)(y * realcellheight + y2 + 1) *
bd.Stride + (int)(x * realcellwidth + x2 + 1) * 4));
}
}
b.UnlockBits(bd2);
Chars[j++] = b;
}
}
SheetBM.UnlockBits(bd);
}
foreach (var charMap in FontInfo.CodeMapDictionary)
{
var idx = charMap.Value;
if (idx == 0xFFFF) continue;
var info = GetCharWidthInfoByIndex(FontInfo, (ushort)idx);
f.Characters.Add(charMap.Key, new BitmapFont.Character(Chars[idx], info.Left, info.GlyphWidth, info.CharWidth));
}
return f;
}
private CharacterWidthEntry GetCharWidthInfoByIndex(FINF fontInfo, UInt16 Index)
{
foreach (var v in fontInfo.CharacterWidths)
{
if (Index < v.StartIndex || Index > v.EndIndex) continue;
return v.WidthEntries[Index - v.StartIndex];
}
return null;
}
}
//Kerning Table
//https://github.com/dnasdw/3dsfont/blob/4ead538d225d5d05929dce9d736bec91a6158052/src/bffnt/ResourceFormat.h
public class FontKerningTable
{
private byte[] Data;
public KerningFirstTable FirstTable { get; set; }
public void Read(FileReader reader, FFNT Header)
{
if (Header.Platform == FFNT.PlatformType.NX)
{
ushort FirstWordCount = reader.ReadUInt16();
ushort padding = reader.ReadUInt16();
FirstTable = new KerningFirstTable();
FirstTable.Read(reader, Header);
}
}
}
public class KerningFirstTable
{
public uint FirstWordCount { get; set; }
public uint Offset { get; set; }
public void Read(FileReader reader, FFNT Header)
{
if (Header.Platform == FFNT.PlatformType.NX)
{
uint FirstWordCount = reader.ReadUInt16();
uint Offset = reader.ReadUInt16();
}
}
}
public enum Gx2ImageFormats
{
RGBA8_UNORM,
RGB8_UNORM,
RGB5A1_UNORM,
RGB565_UNORM,
RGBA4_UNORM,
LA8_UNORM,
LA4_UNORM,
A4_UNORM,
A8_UNORM,
BC1_UNORM,
BC2_UNORM,
BC3_UNORM,
BC4_UNORM,
BC5_UNORM,
RGBA8_SRGB,
BC1_SRGB,
BC2_SRGB,
BC3_SRGB,
}
public class Gx2ImageBlock : STGenericTexture
{
public TGLP TextureTGLP;
public int SheetIndex = 0;
public void Load(TGLP texture, int Index)
{
CanReplace = true;
SheetIndex = Index;
TextureTGLP = texture;
Height = TextureTGLP.SheetHeight;
Width = TextureTGLP.SheetWidth;
var BFNTFormat = (Gx2ImageFormats)TextureTGLP.Format;
Format = ConvertToGeneric(BFNTFormat);
if (Format == TEX_FORMAT.BC4_UNORM)
{
AlphaChannel = STChannelType.Red;
}
ImageKey = "Texture";
SelectedImageKey = "Texture";
}
public override bool CanEdit { get; set; } = true;
public override string ExportFilter => FileFilters.GTX;
public override string ReplaceFilter => FileFilters.GTX;
public override void Replace(string FileName)
{
Bfres.Structs.FTEX ftex = new Bfres.Structs.FTEX();
ftex.ReplaceTexture(FileName, Format, 1, 0, SupportedFormats, true, true, false);
if (ftex.texture != null)
{
TextureTGLP.Format = (ushort)ConvertToGx2(ftex.Format);
TextureTGLP.SheetHeight = (ushort)ftex.texture.Height;
TextureTGLP.SheetWidth = (ushort)ftex.texture.Width;
TextureTGLP.SheetDataList[SheetIndex] = ftex.texture.Data;
Format = ftex.Format;
Width = ftex.texture.Width;
Height = ftex.texture.Height;
UpdateEditor();
}
}
public override TEX_FORMAT[] SupportedFormats
{
get
{
return new TEX_FORMAT[] {
TEX_FORMAT.R8_UNORM,
TEX_FORMAT.BC1_UNORM_SRGB,
TEX_FORMAT.BC1_UNORM,
TEX_FORMAT.BC2_UNORM,
TEX_FORMAT.BC2_UNORM_SRGB,
TEX_FORMAT.BC3_UNORM,
TEX_FORMAT.BC3_UNORM_SRGB,
TEX_FORMAT.BC4_UNORM,
TEX_FORMAT.BC5_UNORM,
TEX_FORMAT.R8G8_UNORM,
TEX_FORMAT.B5G6R5_UNORM,
TEX_FORMAT.B5G5R5A1_UNORM,
TEX_FORMAT.R8G8B8A8_UNORM_SRGB,
TEX_FORMAT.R8G8B8A8_UNORM,
};
}
}
public TEX_FORMAT ConvertToGeneric(Gx2ImageFormats Format)
{
switch (Format)
{
case Gx2ImageFormats.A8_UNORM: return TEX_FORMAT.R8_UNORM;
case Gx2ImageFormats.BC1_SRGB: return TEX_FORMAT.BC1_UNORM_SRGB;
case Gx2ImageFormats.BC1_UNORM: return TEX_FORMAT.BC1_UNORM;
case Gx2ImageFormats.BC2_UNORM: return TEX_FORMAT.BC2_UNORM;
case Gx2ImageFormats.BC2_SRGB: return TEX_FORMAT.BC2_UNORM_SRGB;
case Gx2ImageFormats.BC3_UNORM: return TEX_FORMAT.BC3_UNORM;
case Gx2ImageFormats.BC3_SRGB: return TEX_FORMAT.BC3_UNORM_SRGB;
case Gx2ImageFormats.BC4_UNORM: return TEX_FORMAT.BC4_UNORM;
case Gx2ImageFormats.BC5_UNORM: return TEX_FORMAT.BC5_UNORM;
case Gx2ImageFormats.LA4_UNORM: return TEX_FORMAT.R4G4_UNORM;
case Gx2ImageFormats.LA8_UNORM: return TEX_FORMAT.R8G8_UNORM;
case Gx2ImageFormats.RGB565_UNORM: return TEX_FORMAT.B5G6R5_UNORM;
case Gx2ImageFormats.RGB5A1_UNORM: return TEX_FORMAT.B5G5R5A1_UNORM;
case Gx2ImageFormats.RGB8_UNORM: return TEX_FORMAT.R8G8_UNORM;
case Gx2ImageFormats.RGBA8_SRGB: return TEX_FORMAT.R8G8B8A8_UNORM_SRGB;
case Gx2ImageFormats.RGBA8_UNORM: return TEX_FORMAT.R8G8B8A8_UNORM;
default:
throw new NotImplementedException("Unsupported format " + Format);
}
}
public Gx2ImageFormats ConvertToGx2(TEX_FORMAT Format)
{
switch (Format)
{
case TEX_FORMAT.R8_UNORM: return Gx2ImageFormats.A8_UNORM;
case TEX_FORMAT.BC1_UNORM_SRGB: return Gx2ImageFormats.BC1_SRGB;
case TEX_FORMAT.BC1_UNORM: return Gx2ImageFormats.BC1_UNORM;
case TEX_FORMAT.BC2_UNORM_SRGB: return Gx2ImageFormats.BC2_SRGB;
case TEX_FORMAT.BC2_UNORM: return Gx2ImageFormats.BC2_UNORM;
case TEX_FORMAT.BC3_UNORM_SRGB: return Gx2ImageFormats.BC3_SRGB;
case TEX_FORMAT.BC3_UNORM: return Gx2ImageFormats.BC3_UNORM;
case TEX_FORMAT.BC4_UNORM: return Gx2ImageFormats.BC4_UNORM;
case TEX_FORMAT.BC5_UNORM: return Gx2ImageFormats.BC5_UNORM;
case TEX_FORMAT.R4G4_UNORM: return Gx2ImageFormats.LA4_UNORM;
case TEX_FORMAT.R8G8_UNORM: return Gx2ImageFormats.RGB8_UNORM;
case TEX_FORMAT.B5G6R5_UNORM: return Gx2ImageFormats.RGB565_UNORM;
case TEX_FORMAT.B5G5R5A1_UNORM: return Gx2ImageFormats.RGB5A1_UNORM;
case TEX_FORMAT.R8G8B8A8_UNORM_SRGB: return Gx2ImageFormats.RGBA8_SRGB;
case TEX_FORMAT.R8G8B8A8_UNORM: return Gx2ImageFormats.RGBA8_UNORM;
default:
throw new NotImplementedException("Unsupported format " + Format);
}
}
public override void SetImageData(Bitmap bitmap, int ArrayLevel)
{
if (bitmap == null)
return; //Image is likely disposed and not needed to be applied
uint Gx2Format = (uint)Bfres.Structs.FTEX.ConvertToGx2Format(Format);
Width = (uint)bitmap.Width;
Height = (uint)bitmap.Height;
MipCount = 1;
uint[] MipOffsets = new uint[MipCount];
try
{
//Create image block from bitmap first
var data = GenerateMipsAndCompress(bitmap, MipCount, Format);
//Swizzle and create surface
var surface = GX2.CreateGx2Texture(data, Text,
(uint)4,
(uint)0,
(uint)Width,
(uint)Height,
(uint)1,
(uint)Gx2Format,
(uint)SwizzlePattern,
(uint)1,
(uint)MipCount
);
TextureTGLP.Format = (ushort)ConvertToGx2(Format);
TextureTGLP.SheetHeight = (ushort)surface.height;
TextureTGLP.SheetWidth = (ushort)surface.width;
TextureTGLP.SheetDataList[SheetIndex] = surface.data;
IsEdited = true;
UpdateEditor();
}
catch (Exception ex)
{
STErrorDialog.Show("Failed to swizzle and compress image " + Text, "Error", ex.ToString());
}
}
private const uint SwizzleBase = 0x00000000;
private uint swizzle;
private uint Swizzle
{
get
{
swizzle = SwizzleBase;
swizzle |= (uint)(SheetIndex * 2) << 8;
return swizzle;
}
}
private uint SwizzlePattern
{
get
{
return (uint)(SheetIndex * 2);
}
}
public override byte[] GetImageData(int ArrayLevel = 0, int MipLevel = 0)
{
uint bpp = GetBytesPerPixel(Format);
GX2.GX2Surface surf = new GX2.GX2Surface();
surf.bpp = bpp;
surf.height = Height;
surf.width = Width;
surf.aa = (uint)GX2.GX2AAMode.GX2_AA_MODE_1X;
surf.alignment = 0;
surf.depth = 1;
surf.dim = (uint)GX2.GX2SurfaceDimension.DIM_2D;
surf.format = (uint)Bfres.Structs.FTEX.ConvertToGx2Format(Format);
surf.use = (uint)GX2.GX2SurfaceUse.USE_COLOR_BUFFER;
surf.pitch = 0;
surf.data = TextureTGLP.SheetDataList[SheetIndex];
surf.numMips = 1;
surf.mipOffset = new uint[0];
surf.mipData = null;
surf.tileMode = (uint)GX2.GX2TileMode.MODE_2D_TILED_THIN1;
surf.swizzle = Swizzle;
surf.numArray = 1;
return GX2.Decode(surf, ArrayLevel, MipLevel);
}
public override void OnClick(TreeView treeview)
{
UpdateEditor();
}
private void UpdateEditor()
{
ImageEditorBase editor = (ImageEditorBase)LibraryGUI.GetActiveContent(typeof(ImageEditorBase));
if (editor == null)
{
editor = new ImageEditorBase();
editor.Dock = DockStyle.Fill;
LibraryGUI.LoadEditor(editor);
}
Properties prop = new Properties();
prop.Width = Width;
prop.Height = Height;
prop.Depth = Depth;
prop.MipCount = MipCount;
prop.ArrayCount = ArrayCount;
prop.ImageSize = (uint)TextureTGLP.SheetDataList[SheetIndex].Length;
prop.Format = Format;
prop.Swizzle = Swizzle;
editor.Text = Text;
editor.LoadProperties(prop);
editor.LoadImage(this);
}
}
public class BFFNT_Block
{
}
public class FINF : BFFNT_Block
{
public Dictionary<char, int> CodeMapDictionary = new Dictionary<char, int>();
public uint Size;
public FontType Type { get; set; }
public byte Width { get; set; }
public byte Height { get; set; }
public byte Ascent { get; set; }
public ushort LineFeed { get; set; }
public ushort AlterCharIndex { get; set; }
public byte DefaultLeftWidth { get; set; }
public byte DefaultGlyphWidth { get; set; }
public byte DefaultCharWidth { get; set; }
public CharacterCode CharEncoding { get; set; }
public TGLP TextureGlyph;
public CMAP CodeMap;
public CWDH CharacterWidth;
public List<CWDH> CharacterWidths { get; set; }
public List<CMAP> CodeMaps { get; set; }
public enum FontType : byte
{
Glyph = 1,
Texture = 2,
PackedTexture = 3,
}
public enum CharacterCode : byte
{
Unicode = 1,
ShiftJIS = 2,
CP1252 = 3,
}
public void Read(FileReader reader, FFNT Header)
{
CharacterWidths = new List<CWDH>();
CodeMaps = new List<CMAP>();
string Signature = reader.ReadString(4, Encoding.ASCII);
if (Signature != "FINF")
throw new Exception($"Invalid signature {Signature}! Expected FINF.");
Size = reader.ReadUInt32();
if (Header.Platform == FFNT.PlatformType.Ctr)
{
Type = reader.ReadEnum<FontType>(true);
LineFeed = reader.ReadUInt16();
AlterCharIndex = reader.ReadUInt16();
DefaultLeftWidth = reader.ReadByte();
DefaultGlyphWidth = reader.ReadByte();
DefaultCharWidth = reader.ReadByte();
CharEncoding = reader.ReadEnum<CharacterCode>(true);
uint tglpOffset = reader.ReadUInt32();
uint cwdhOffset = reader.ReadUInt32();
uint cmapOffset = reader.ReadUInt32();
Height = reader.ReadByte();
Width = reader.ReadByte();
Ascent = reader.ReadByte();
reader.ReadByte(); //Padding
//Add counter for TGLP
//Note the other counters are inside sections due to recusive setup
Header.BlockCounter += 1;
TextureGlyph = new TGLP();
using (reader.TemporarySeek(tglpOffset - 8, SeekOrigin.Begin))
TextureGlyph.Read(reader);
CharacterWidth = new CWDH();
CharacterWidths.Add(CharacterWidth);
using (reader.TemporarySeek(cwdhOffset - 8, SeekOrigin.Begin))
CharacterWidth.Read(reader, Header, CharacterWidths);
CodeMap = new CMAP();
CodeMaps.Add(CodeMap);
using (reader.TemporarySeek(cmapOffset - 8, SeekOrigin.Begin))
CodeMap.Read(reader, Header, CodeMaps);
}
else
{
Type = reader.ReadEnum<FontType>(true);
Height = reader.ReadByte();
Width = reader.ReadByte();
Ascent = reader.ReadByte();
LineFeed = reader.ReadUInt16();
AlterCharIndex = reader.ReadUInt16();
DefaultLeftWidth = reader.ReadByte();
DefaultGlyphWidth = reader.ReadByte();
DefaultCharWidth = reader.ReadByte();
CharEncoding = reader.ReadEnum<CharacterCode>(true);
uint tglpOffset = reader.ReadUInt32();
uint cwdhOffset = reader.ReadUInt32();
uint cmapOffset = reader.ReadUInt32();
//Add counter for TGLP
//Note the other counters are inside sections due to recusive setup
Header.BlockCounter += 1;
TextureGlyph = new TGLP();
using (reader.TemporarySeek(tglpOffset - 8, SeekOrigin.Begin))
TextureGlyph.Read(reader);
CharacterWidth = new CWDH();
CharacterWidths.Add(CharacterWidth);
using (reader.TemporarySeek(cwdhOffset - 8, SeekOrigin.Begin))
CharacterWidth.Read(reader, Header, CharacterWidths);
CodeMap = new CMAP();
CodeMaps.Add(CodeMap);
using (reader.TemporarySeek(cmapOffset - 8, SeekOrigin.Begin))
CodeMap.Read(reader, Header, CodeMaps);
}
}
public void Write(FileWriter writer, FFNT header)
{
long pos = writer.Position;
writer.WriteSignature("FINF");
writer.Write(uint.MaxValue);
writer.Write(Type, true);
writer.Write(Height);
writer.Write(Width);
writer.Write(Ascent);
writer.Write(LineFeed);
writer.Write(AlterCharIndex);
writer.Write(DefaultLeftWidth);
writer.Write(DefaultGlyphWidth);
writer.Write(DefaultCharWidth);
writer.Write(CharEncoding, true);
long _ofsTGLP = writer.Position;
writer.Write(uint.MaxValue);
long _ofsCWDH = writer.Position;
writer.Write(uint.MaxValue);
long _ofsCMAP = writer.Position;
writer.Write(uint.MaxValue);
//Save section size
long endPos = writer.Position;
using (writer.TemporarySeek(pos + 4, SeekOrigin.Begin))
{
writer.Write((uint)(endPos - pos));
}
//Save Texture Glyph
writer.WriteUint32Offset(_ofsTGLP, -8);
TextureGlyph.Write(writer, header);
//Save Character Widths
writer.WriteUint32Offset(_ofsCWDH, -8);
CharacterWidth.Write(writer, header);
//Save Code Maps
writer.WriteUint32Offset(_ofsCMAP, -8);
CodeMap.Write(writer, header);
}
public CWDH GetCharacterWidth(int index)
{
if (index == -1)
return null;
for (int i = 0; i < CharacterWidths.Count; i++)
{
if (CharacterWidths[i].StartIndex <= index && CharacterWidths[i].EndIndex >= index)
{
int CharaIndex = index - CharacterWidths[i].StartIndex;
return CharacterWidths[CharaIndex];
}
}
throw new Exception("Failed to get valid character index!");
}
}
public class TGLP
{
public BNTX BinaryTextureFile;
public List<Gx2ImageBlock> Gx2Textures = new List<Gx2ImageBlock>();
public uint SectionSize;
public byte CellWidth { get; set; }
public byte CellHeight { get; set; }
public byte MaxCharWidth { get; set; }
public byte SheetCount { get; private set; }
public uint SheetSize { get; set; }
public ushort BaseLinePos { get; set; }
public ushort Format { get; set; }
public ushort RowCount { get; set; }
public ushort LinesCount { get; set; }
public ushort SheetWidth { get; set; }
public ushort SheetHeight { get; set; }
public List<byte[]> SheetDataList = new List<byte[]>();
public void Read(FileReader reader)
{
string Signature = reader.ReadString(4, Encoding.ASCII);
if (Signature != "TGLP")
throw new Exception($"Invalid signature {Signature}! Expected TGLP.");
SectionSize = reader.ReadUInt32();
CellWidth = reader.ReadByte();
CellHeight = reader.ReadByte();
SheetCount = reader.ReadByte();
MaxCharWidth = reader.ReadByte();
SheetSize = reader.ReadUInt32();
BaseLinePos = reader.ReadUInt16();
Format = reader.ReadUInt16();
RowCount = reader.ReadUInt16();
LinesCount = reader.ReadUInt16();
SheetWidth = reader.ReadUInt16();
SheetHeight = reader.ReadUInt16();
uint sheetOffset = reader.ReadUInt32();
using (reader.TemporarySeek(sheetOffset, SeekOrigin.Begin))
{
for (int i = 0; i < SheetCount; i++)
{
SheetDataList.Add(reader.ReadBytes((int)SheetSize));
}
}
}
public void Write(FileWriter writer, FFNT Header)
{
long pos = writer.Position;
if (BinaryTextureFile != null)
{
var mem = new System.IO.MemoryStream();
BinaryTextureFile.Save(mem);
SheetDataList[0] = mem.ToArray();
}
writer.WriteSignature("TGLP");
writer.Write(uint.MaxValue);
writer.Write(CellWidth);
writer.Write(CellHeight);
writer.Write((byte)SheetDataList.Count);
writer.Write(MaxCharWidth);
writer.Write(SheetDataList[0].Length);
writer.Write(BaseLinePos);
writer.Write(Format);
writer.Write(RowCount);
writer.Write(LinesCount);
writer.Write(SheetWidth);
writer.Write(SheetHeight);
long _ofsSheetBlocks = writer.Position;
writer.Write(uint.MaxValue);
if (Header.Platform == FFNT.PlatformType.NX)
writer.Align(4096);
else
writer.Align(8192);
long DataPosition = writer.Position;
using (writer.TemporarySeek(_ofsSheetBlocks, SeekOrigin.Begin))
{
writer.Write((uint)DataPosition);
}
for (int i = 0; i < SheetDataList.Count; i++)
{
writer.Write(SheetDataList[i]);
}
long SectionEndPosition = writer.Position;
//End of section. Set the size
using (writer.TemporarySeek(pos + 4, SeekOrigin.Begin))
{
writer.Write((uint)(SectionEndPosition - pos));
}
}
public STGenericTexture[] GetImageSheets()
{
STGenericTexture[] textures = new STGenericTexture[SheetCount];
for (int i = 0; i < SheetCount; i++)
textures[i] = GetImageSheet(i);
return textures;
}
public STGenericTexture GetImageSheet(int Index)
{
if (BinaryTextureFile != null) //BNTX uses only one image with multiple arrays
return BinaryTextureFile.Textures.ElementAt(0).Value;
else
return Gx2Textures[Index];
}
}
public interface CharMapping { }
public class CMAPIndexTable : CharMapping
{
public short[] Table { get; set; }
}
public class CMAPDirect : CharMapping
{
public ushort Offset { get; set; }
}
public class CMAPScanMapping : CharMapping
{
public uint[] Codes { get; set; }
public short[] Indexes { get; set; }
}
public class CMAP
{
public uint SectionSize;
public char CharacterCodeBegin { get; set; }
public char CharacterCodeEnd { get; set; }
public Mapping MappingMethod { get; set; }
private ushort Padding;
public CharMapping MappingData;
public enum Mapping : ushort
{
Direct,
Table,
Scan,
}
public ushort GetIndexFromCode(ushort code)
{
if (code < CharacterCodeBegin || code > CharacterCodeEnd) return 0xFFFF;
switch (MappingMethod)
{
case Mapping.Direct:
return (UInt16)(code - CharacterCodeBegin + ((CMAPDirect)MappingData).Offset);
case Mapping.Table:
return (ushort)((CMAPIndexTable)MappingData).Table[code - CharacterCodeBegin];
case Mapping.Scan:
if (!((CMAPScanMapping)MappingData).Codes.Contains(code)) return 0xFFFF;
else
{
var codes = ((CMAPScanMapping)MappingData).Codes;
var index = Array.FindIndex(codes, map => map == code);
return (ushort)((CMAPScanMapping)MappingData).Indexes[index];
}
}
return 0xFFFF;
}
public CMAP NextCodeMapSection { get; set; }
public void Read(FileReader reader, FFNT header, List<CMAP> CodeMaps)
{
uint CodeBegin = 0;
uint CodeEnd = 0;
long pos = reader.Position;
reader.ReadSignature(4, "CMAP");
SectionSize = reader.ReadUInt32();
if (header.Platform == FFNT.PlatformType.NX)
{
CodeBegin = reader.ReadUInt32();
CodeEnd = reader.ReadUInt32();
MappingMethod = reader.ReadEnum<Mapping>(true);
Padding = reader.ReadUInt16();
}
else
{
CodeBegin = reader.ReadUInt16();
CodeEnd = reader.ReadUInt16();
MappingMethod = reader.ReadEnum<Mapping>(true);
Padding = reader.ReadUInt16();
}
CharacterCodeBegin = (char)CodeBegin;
CharacterCodeEnd = (char)CodeEnd;
uint NextMapOffset = reader.ReadUInt32();
//Mapping methods from
https://github.com/IcySon55/Kuriimu/blob/f670c2719affc1eaef8b4c40e40985881247acc7/src/Cetera/Font/BFFNT.cs#L211
switch (MappingMethod)
{
case Mapping.Direct:
var charOffset = reader.ReadUInt16();
for (char i = CharacterCodeBegin; i <= CharacterCodeEnd; i++)
{
int idx = i - CharacterCodeBegin + charOffset;
header.FontSection.CodeMapDictionary[i] = idx < ushort.MaxValue ? idx : 0;
}
MappingData = new CMAPDirect();
((CMAPDirect)MappingData).Offset = charOffset;
break;
case Mapping.Table:
List<short> table = new List<short>();
for (char i = CharacterCodeBegin; i <= CharacterCodeEnd; i++)
{
short idx = reader.ReadInt16();
if (idx != -1) header.FontSection.CodeMapDictionary[i] = idx;
table.Add(idx);
}
MappingData = new CMAPIndexTable();
((CMAPIndexTable)MappingData).Table = table.ToArray();
break;
case Mapping.Scan:
var CharEntryCount = reader.ReadUInt16();
if (header.Platform == FFNT.PlatformType.NX)
reader.ReadUInt16(); //Padding
uint[] codes = new uint[CharEntryCount];
short[] indexes = new short[CharEntryCount];
for (int i = 0; i < CharEntryCount; i++)
{
if (header.Platform == FFNT.PlatformType.NX)
{
uint charCode = reader.ReadUInt32();
short index = reader.ReadInt16();
short padding = reader.ReadInt16();
if (index != -1) header.FontSection.CodeMapDictionary[(char)charCode] = index;
codes[i] = charCode;
indexes[i] = index;
}
else
{
ushort charCode = reader.ReadUInt16();
short index = reader.ReadInt16();
if (index != -1) header.FontSection.CodeMapDictionary[(char)charCode] = index;
codes[i] = charCode;
indexes[i] = index;
}
}
MappingData = new CMAPScanMapping();
((CMAPScanMapping)MappingData).Codes = codes;
((CMAPScanMapping)MappingData).Indexes = indexes;
break;
}
if (NextMapOffset != 0)
{
reader.SeekBegin(NextMapOffset - 8);
NextCodeMapSection = new CMAP();
NextCodeMapSection.Read(reader, header, CodeMaps);
CodeMaps.Add(NextCodeMapSection);
}
else
reader.SeekBegin(pos + SectionSize);
}
public void Write(FileWriter writer, FFNT Header)
{
Header.BlockCounter += 1;
long pos = writer.Position;
writer.WriteSignature("CMAP");
writer.Write(uint.MaxValue); //Section Size
if (Header.Platform == FFNT.PlatformType.NX)
{
writer.Write((uint)CharacterCodeBegin);
writer.Write((uint)CharacterCodeEnd);
}
else
{
writer.Write((ushort)CharacterCodeBegin);
writer.Write((ushort)CharacterCodeEnd);
}
writer.Write(MappingMethod, true);
writer.Seek(2);
long DataPos = writer.Position;
writer.Write(0); //Next Section Offset
//Write the data
switch (MappingMethod)
{
case Mapping.Direct:
writer.Write(((CMAPDirect)MappingData).Offset);
break;
case Mapping.Table:
for (int i = 0; i < ((CMAPIndexTable)MappingData).Table.Length; i++)
{
writer.Write(((CMAPIndexTable)MappingData).Table[i]);
}
break;
case Mapping.Scan:
writer.Write((ushort)((CMAPScanMapping)MappingData).Codes.Length);
if (Header.Platform == FFNT.PlatformType.NX)
writer.Seek(2); //Padding
for (int i = 0; i < ((CMAPScanMapping)MappingData).Codes.Length; i++)
{
if (Header.Platform == FFNT.PlatformType.NX)
{
writer.Write((uint)((CMAPScanMapping)MappingData).Codes[i]);
writer.Write(((CMAPScanMapping)MappingData).Indexes[i]);
writer.Write((ushort)0); //Padding
}
else
{
writer.Write((ushort)((CMAPScanMapping)MappingData).Codes[i]);
writer.Write(((CMAPScanMapping)MappingData).Indexes[i]);
}
}
break;
}
writer.Align(4); //Padding
//Save section size
long endPos = writer.Position;
using (writer.TemporarySeek(pos + 4, SeekOrigin.Begin))
{
writer.Write((uint)(endPos - pos));
}
if (NextCodeMapSection != null)
{
writer.WriteUint32Offset(DataPos, -8);
NextCodeMapSection.Write(writer, Header);
}
}
//From https://github.com/dnasdw/3dsfont/blob/79e6f4ab6676d82fdcd6c0f79d9b0d7a343f82b5/src/bcfnt2charset/bcfnt2charset.cpp#L3
//Todo add the rest of the encoding types
public char CodeToU16Code(FINF.CharacterCode characterCode, ushort code)
{
if (code < 0x20)
{
return (char)0;
}
switch (characterCode)
{
case FINF.CharacterCode.Unicode:
return (char)code;
}
return (char)code;
}
}
public class CWDH
{
public ushort StartIndex { get; set; }
public ushort EndIndex { get; set; }
public List<CharacterWidthEntry> WidthEntries = new List<CharacterWidthEntry>();
public CWDH NextWidthSection { get; set; }
public ushort EntryCount
{
get { return (ushort)(EndIndex - StartIndex + 1); }
}
public uint SectionSize;
public void Read(FileReader reader, FFNT header, List<CWDH> CharacterWidths)
{
long pos = reader.Position;
reader.ReadSignature(4, "CWDH");
SectionSize = reader.ReadUInt32();
StartIndex = reader.ReadUInt16();
EndIndex = reader.ReadUInt16();
uint NextWidthSectionOffset = reader.ReadUInt32();
for (ushort i = StartIndex; i <= EndIndex; i++)
{
var entry = new CharacterWidthEntry();
entry.Left = reader.ReadSByte();
entry.GlyphWidth = reader.ReadByte();
entry.CharWidth = reader.ReadByte();
WidthEntries.Add(entry);
}
if (NextWidthSectionOffset != 0)
{
reader.SeekBegin((int)NextWidthSectionOffset - 8);
NextWidthSection = new CWDH();
NextWidthSection.Read(reader, header, CharacterWidths);
CharacterWidths.Add(NextWidthSection);
}
else
reader.SeekBegin(pos + SectionSize);
}
public void Write(FileWriter writer, FFNT Header)
{
Header.BlockCounter += 1;
long pos = writer.Position;
writer.WriteSignature("CWDH");
writer.Write(uint.MaxValue); //Section Size
writer.Write(StartIndex);
writer.Write(EndIndex);
long DataPos = writer.Position;
writer.Write(0); //NextOffset
for (int i = 0; i < WidthEntries.Count; i++)
{
writer.Write(WidthEntries[i].Left);
writer.Write(WidthEntries[i].GlyphWidth);
writer.Write(WidthEntries[i].CharWidth);
}
writer.Align(4);
if (NextWidthSection != null)
{
writer.WriteUint32Offset(DataPos, -8);
NextWidthSection.Write(writer, Header);
}
//Save section size
long endPos = writer.Position;
using (writer.TemporarySeek(pos + 4, SeekOrigin.Begin))
{
writer.Write((uint)(endPos - pos));
}
}
}
public class CharacterWidthEntry
{
public sbyte Left { get; set; }
public byte GlyphWidth { get; set; }
public byte CharWidth { get; set; }
}
}