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, 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 types = new List(); 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 Blocks = new List(); 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; int fontWidth = (int)textPane.FontSize.X; int fontHeight = (int)textPane.FontSize.Y; if (textPane.FontSize.X > 2) { fontWidth = (int)textPane.FontSize.X - 2; fontHeight = (int)textPane.FontSize.Y - 2; } float XScale = (fontWidth / TextureGlyph.CellWidth); float YScale = (fontHeight / TextureGlyph.CellWidth); float height = (TextureGlyph.SheetHeight - 2) / TextureGlyph.LinesCount; /* int pos = 0; for (int i = 0; i < text.Length; i++) { char character = text[i]; int charWidth = (int)FontInfo.DefaultCharWidth; int glyphWidth = (int)FontInfo.DefaultGlyphWidth; int leftWidth = (int)FontInfo.DefaultLeftWidth; if (FontInfo.CodeMapDictionary.ContainsKey(character)) { var idx = FontInfo.CodeMapDictionary[character]; if (idx == 0xFFFF) continue; var charWidthInfo = GetCharWidthInfoByIndex(FontInfo, (ushort)idx); charWidth = charWidthInfo.CharWidth; glyphWidth = charWidthInfo.GlyphWidth; leftWidth = charWidthInfo.Left; } /* Bitmap b = new Bitmap(width, height); using (Graphics g = Graphics.FromImage(b)) { g.DrawImage(); } }*/ if (bitmapFont == null) bitmapFont = GetBitmapFont(true); return bitmapFont.PrintToBitmap(text, new BitmapFont.FontRenderSettings() { TopColor = textPane.FontTopColor.Color, BottomColor = textPane.FontBottomColor.Color, 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, int DepthLevel = 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 CodeMapDictionary = new Dictionary(); 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 CharacterWidths { get; set; } public List 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(); CodeMaps = new List(); 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(true); LineFeed = reader.ReadUInt16(); AlterCharIndex = reader.ReadUInt16(); DefaultLeftWidth = reader.ReadByte(); DefaultGlyphWidth = reader.ReadByte(); DefaultCharWidth = reader.ReadByte(); CharEncoding = reader.ReadEnum(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(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(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 Gx2Textures = new List(); 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 SheetDataList = new List(); 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 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(true); Padding = reader.ReadUInt16(); } else { CodeBegin = reader.ReadUInt16(); CodeEnd = reader.ReadUInt16(); MappingMethod = reader.ReadEnum(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 table = new List(); 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 WidthEntries = new List(); 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 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; } } }