From 84f13b42c2ac74be269da1dcc61332621faa3c4a Mon Sep 17 00:00:00 2001 From: MIRIMIRIM <59959583+MIRIMIRIM@users.noreply.github.com> Date: Sat, 11 May 2024 20:58:15 +0800 Subject: [PATCH] afs.core: Rewrite the method of matching ass font info with physical font info --- AssFontSubset.Core/src/AssFont.cs | 97 +++++++++++++---- AssFontSubset.Core/src/FontParse.cs | 97 +++++------------ AssFontSubset.Core/src/SubsetByPyFT.cs | 74 +++---------- AssFontSubset.CoreTests/src/AssFontTests.cs | 113 +++++++++++++++----- 4 files changed, 207 insertions(+), 174 deletions(-) diff --git a/AssFontSubset.Core/src/AssFont.cs b/AssFontSubset.Core/src/AssFont.cs index f992b5c..696a41c 100644 --- a/AssFontSubset.Core/src/AssFont.cs +++ b/AssFontSubset.Core/src/AssFont.cs @@ -2,6 +2,7 @@ using Mobsub.SubtitleParse; using Mobsub.SubtitleParse.AssTypes; using System.Text; +using ZLogger; namespace AssFontSubset.Core; @@ -23,45 +24,95 @@ public static Dictionary> GetAssFonts(string file, out A return AssFontParse.GetUsedFontInfos(ass.Events.Collection, ass.Styles.Collection); } - public static bool IsMatch(AssFontInfo afi, FontInfo fi) + public static bool IsMatch(AssFontInfo afi, FontInfo fi, bool single, int? minimalWeight = null, bool? hadItalic = null, ILogger? logger = null) { var boldMatch = false; var italicMatch = false; + if (!single) { if (minimalWeight is null || hadItalic is null) throw new ArgumentNullException(); } - var assFn = afi.Name.StartsWith('@') ? afi.Name.AsSpan(1) : afi.Name.AsSpan(); - if ((assFn.SequenceEqual(fi.FamilyName.AsSpan()) || assFn.SequenceEqual(fi.FamilyNameChs.AsSpan()))) + logger?.ZLogDebug($"Try match {afi.ToString()} and {fi.FamilyName}_w{fi.Weight}_b{(fi.Bold ? 1 : 0)}_i{(fi.Italic ? 1 : 0)}"); + switch (afi.Weight) { - if (afi.Weight == 0) - { - boldMatch = !fi.Bold; - } - else if (afi.Weight == 1) - { - boldMatch = fi.Bold || (!fi.MaybeHasTrueBoldOrItalic && !fi.Bold && !fi.Italic); - } - else if (afi.Weight == fi.Weight) - { - // Maybe wrong - boldMatch = true; - } + case 0: + boldMatch = fi.Bold ? single : true; // cant get only true bold + break; + case 1: + if (single) + { + // maybe faux bold + if (fi.Weight >= 550) { logger?.ZLogWarning($"{afi.Name} use \\b1 will not get faux bold"); } + boldMatch = true; + } + else + { + // strict + boldMatch = fi.Bold; + } + break; + default: + if (afi.Weight == fi.Weight) + { + boldMatch = true; + } + else + { + if (fi.Weight > (afi.Weight + 150)) { logger?.ZLogDebug($"{afi.Name} should use \\b{fi.Weight}"); } + } + break; + } - if (afi.Italic == fi.Italic) - { - italicMatch = true; - } - else if (afi.Italic == true && (!fi.MaybeHasTrueBoldOrItalic && !fi.Bold && !fi.Italic)) + if (afi.Italic) + { + if (fi.Italic) { italicMatch = true; } - else if (afi.Italic == true && fi.MaybeHasTrueBoldOrItalic && fi.FamilyName != fi.FamilyNameChs) + else { - italicMatch = true; + // maybe faux italic + if (single) { italicMatch = true; } + else + { + if (!(bool)hadItalic!) { italicMatch = true; } + else if (!(fi.MaxpNumGlyphs < 6000 && fi.FamilyName == fi.FamilyNameChs)) + { + // maybe cjk fonts + italicMatch = true; + logger?.ZLogDebug($"{afi.Name} use \\i1 maybe get faux italic"); + } + } } } + else + { + if (!fi.Italic) { italicMatch = true; } + } return boldMatch && italicMatch; } + public static FontInfo? GetMatchedFontInfo(AssFontInfo afi, IGrouping fig, ILogger? logger = null) + { + var assFn = afi.Name.StartsWith('@') ? afi.Name.AsSpan(1) : afi.Name.AsSpan(); + if (!(assFn.SequenceEqual(fig.Key.AsSpan()) || assFn.SequenceEqual(fig.First().FamilyNameChs.AsSpan()))) { return null; } + + if (fig.Count() == 1) + { + if (IsMatch(afi, fig.First(), true, null, null, logger)) { return fig.First(); } + else { return null; } + } + else + { + var minimalWeight = fig.Select(fig => fig.Weight).Min(); + var hadItalic = fig.Select(fig => fig.Italic is true).Count() > 0; + foreach (var fi in fig) + { + if (IsMatch(afi, fi, false, minimalWeight, hadItalic, logger)) { return fi; } + } + return null; + } + } + private static HashSet GetUsedStyles(List events) { var styles = new HashSet(); diff --git a/AssFontSubset.Core/src/FontParse.cs b/AssFontSubset.Core/src/FontParse.cs index f0505b8..1764f1d 100644 --- a/AssFontSubset.Core/src/FontParse.cs +++ b/AssFontSubset.Core/src/FontParse.cs @@ -7,10 +7,11 @@ public struct FontInfo { public string FamilyName; public string FamilyNameChs; + //public bool Regular; public bool Bold; public bool Italic; public int Weight; - public bool MaybeHasTrueBoldOrItalic; + //public bool MaybeHasTrueBoldOrItalic; public string FileName; public uint Index; public ushort MaxpNumGlyphs; @@ -20,10 +21,11 @@ public override bool Equals(object? obj) return obj is FontInfo info && FamilyName == info.FamilyName && FamilyNameChs == info.FamilyNameChs && + //Regular == info.Regular && Bold == info.Bold && Italic == info.Italic && Weight == info.Weight && - MaybeHasTrueBoldOrItalic == info.MaybeHasTrueBoldOrItalic && + //MaybeHasTrueBoldOrItalic == info.MaybeHasTrueBoldOrItalic && FileName == info.FileName && Index == info.Index && MaxpNumGlyphs == info.MaxpNumGlyphs; @@ -34,10 +36,11 @@ public override int GetHashCode() HashCode hash = new HashCode(); hash.Add(FamilyName); hash.Add(FamilyNameChs); + //hash.Add(Regular); hash.Add(Bold); hash.Add(Italic); hash.Add(Weight); - hash.Add(MaybeHasTrueBoldOrItalic); + //hash.Add(MaybeHasTrueBoldOrItalic); hash.Add(FileName); hash.Add(Index); hash.Add(MaxpNumGlyphs); @@ -58,98 +61,54 @@ public class FontParse(string fontFile) public uint GetNumFonts() => FontData.GetNumFonts(); public OTFont GetFont(uint index) => FontData.GetFont(index)!; - public FontInfo GetFontInfo(uint index, HashSet? trueRecord = null) + public FontInfo GetFontInfo(uint index) { var font = GetFont(index); - var infoFileBased = GetFontInfo(font); - var familyName = infoFileBased["family_name"]; - var weight = int.Parse(infoFileBased["weight"]); - - if (!infoFileBased.TryGetValue("family_name_loc", out var familyNameLoc)) - { - familyNameLoc = familyName; - } - - var infoAssLike = new FontInfo() - { - FamilyName = familyName, - FamilyNameChs = familyNameLoc, - Bold = false, - Italic = false, - Weight = weight, - MaybeHasTrueBoldOrItalic = false, - FileName = FontFile, - Index = index, - MaxpNumGlyphs = font.GetMaxpNumGlyphs(), - }; - - if (infoFileBased["subfamily_name"].Contains("Bold")) - { - // 600 DB maybe regular+bold - if (weight == 700 || weight == 600) - { - // maybe only sign style (such as morisawa), normal is DB/B/ED, hanyi use 75J/F/W/S - // UD Digi Kyokasho N-B maybe correct regular+bold - string[] boldIndicators = [" B", " DB", " EB", "75W", "75S", "75J", "75F"]; - // but some morisawa fonts is weird, such as A-OTF Jun Pro 501, will exclude all - string[] excludedPrefixes = ["A-OTF", "A P-OTF", "G-OTF"]; - - if (!(boldIndicators.Any(familyName.EndsWith) || excludedPrefixes.Any(familyName.StartsWith))) - { - infoAssLike.Bold = true; - trueRecord?.Add(familyName); - } - } - } - - if (infoFileBased["subfamily_name"].Contains("Italic")) - { - infoAssLike.Italic = true; - trueRecord?.Add(familyName); - } - - return infoAssLike; - } - public FontInfo GetFontInfo(uint index) => GetFontInfo(index, null); - - public static Dictionary GetFontInfo(OTFont font) - { var nameTable = (Table_name)font.GetTable("name")!; - //var fullName = nameTable.GetString + var os2Table = (Table_OS2)font.GetTable("OS/2")!; + var fsSel = os2Table.fsSelection; var ids = new Dictionary { { "postscript_name", new GetStringParams { EncID = 0xffff, LangID = (ushort)LanguageIDWindows.en_US, NameID = (ushort)NameID.postScriptName } }, - { "full_name", new GetStringParams { EncID = 0xffff, LangID = (ushort)LanguageIDWindows.en_US, NameID = (ushort)NameID.fullName } }, + //{ "full_name", new GetStringParams { EncID = 0xffff, LangID = (ushort)LanguageIDWindows.en_US, NameID = (ushort)NameID.fullName } }, { "family_name", new GetStringParams { EncID = 0xffff, LangID = (ushort)LanguageIDWindows.en_US, NameID = (ushort)NameID.familyName } }, { "family_name_loc", new GetStringParams { EncID = 0xffff, LangID = (ushort)LanguageIDWindows.zh_Hans_CN, NameID = (ushort)NameID.familyName } }, - { "subfamily_name", new GetStringParams { EncID = 0xffff, LangID = (ushort)LanguageIDWindows.en_US, NameID = (ushort)NameID.subfamilyName } }, + //{ "subfamily_name", new GetStringParams { EncID = 0xffff, LangID = (ushort)LanguageIDWindows.en_US, NameID = (ushort)NameID.subfamilyName } }, }; var result = GetBuffers(nameTable, ids); - var stringDict = new Dictionary(); + var nameDict = new Dictionary(); foreach (var kv in result) { if (kv.Value.buf != null) { var s = DecodeString(kv.Value.curPlatID, kv.Value.curEncID, kv.Value.curLangID, kv.Value.buf); - stringDict.Add(kv.Key, s!); + nameDict.Add(kv.Key, s!); } } - if (stringDict.Count > 0) + var familyName = nameDict["family_name"]; + if (!nameDict.TryGetValue("family_name_loc", out var familyNameLoc)){ familyNameLoc = familyName; } + + return new FontInfo() { - var os2Table = (Table_OS2)font.GetTable("OS/2")!; - stringDict.Add("weight", os2Table.usWeightClass.ToString()); - } - - return stringDict; + FamilyName = familyName, + FamilyNameChs = familyNameLoc, + //Regular = ((fsSel & 0b_0100_0000) >> 6) == 1, // bit 6 + Bold = ((fsSel & 0b_0010_0000) >> 5) == 1, // bit 5 + Italic = (fsSel & 0b_1) == 1, // bit 0 + Weight = os2Table.usWeightClass, + //MaybeHasTrueBoldOrItalic = false, + FileName = FontFile, + Index = index, + MaxpNumGlyphs = font.GetMaxpNumGlyphs(), + }; } - private struct GetStringParams { //public ushort PlatID; diff --git a/AssFontSubset.Core/src/SubsetByPyFT.cs b/AssFontSubset.Core/src/SubsetByPyFT.cs index 9d7a081..6a088bd 100644 --- a/AssFontSubset.Core/src/SubsetByPyFT.cs +++ b/AssFontSubset.Core/src/SubsetByPyFT.cs @@ -48,18 +48,6 @@ await Task.Run(() => }); } - static void GetFontInfo(string fontFile) - { - var fp = new FontParse(fontFile); - if (!fp.Open()) { throw new FileNotFoundException(); }; - - var fontInfos = new Dictionary[fp.GetNumFonts()]; - for (uint i = 0; i < fontInfos.Length; i++) - { - fontInfos[i] = FontParse.GetFontInfo(fp.GetFont(i)!); - } - } - List GetFontInfoFromFiles(string dir) { string[] supportFonts = [".ttf", ".otf", ".ttc", "otc"]; @@ -79,45 +67,13 @@ List GetFontInfoFromFiles(string dir) if (!fp.Open()) { throw new FormatException(); }; for (uint i = 0; i < fp.GetNumFonts(); i++) { - fontInfos.Add(fp.GetFontInfo(i, HasTrueBoldOrItalicRecord)); + fontInfos.Add(fp.GetFontInfo(i)); } } } _stopwatch.Stop(); - var pass1 = _stopwatch.ElapsedMilliseconds; - _logger?.ZLogDebug($"初次扫描和解析完成,用时 {pass1} ms"); - //_stopwatch.Reset(); - _logger?.ZLogDebug($"开始分析记录可能有多种变体的 fontfamily"); - _stopwatch.Restart(); - for (var i = 0; i < fontInfos.Count; i++) - { - var info = fontInfos[i]; - if (!info.Bold && !info.Italic) - { - if (HasTrueBoldOrItalicRecord.Contains(info.FamilyName)) - { - info.MaybeHasTrueBoldOrItalic = true; - fontInfos[i] = info; - _logger?.ZLogDebug($"{info.FileName} 中的 {info.FamilyName} 检测到其他变体"); - } - else - { - string[] prefix = ["Arial", "Avenir Next", "Microsoft YaHei", "Source Han", "Noto", "Yu Gothic"]; - if ((info.Weight == 500 && info.FamilyName.StartsWith("Avenir Next")) - || (info.Weight == 400 && (prefix.Any(info.FamilyName.StartsWith) || (info.FamilyName.StartsWith("FZ") && info.FamilyName.EndsWith("JF")) || (info.MaxpNumGlyphs < 6000 && (info.FamilyName == info.FamilyNameChs))))) - { - info.MaybeHasTrueBoldOrItalic = true; - fontInfos[i] = info; - _logger?.ZLogDebug($"{info.FileName} 中的 {info.FamilyName} 未在现有字体中检测到其他变体"); - } - } - } - } - _stopwatch.Stop(); - _logger?.ZLogDebug($"变体分析完成,用时 {_stopwatch.ElapsedMilliseconds} ms"); - _logger?.ZLogInformation($"字体文件扫描完成,用时 {pass1 + _stopwatch.ElapsedMilliseconds} ms"); + _logger?.ZLogDebug($"字体文件扫描完成,用时 {_stopwatch.ElapsedMilliseconds} ms"); _stopwatch.Reset(); - return fontInfos; } @@ -168,21 +124,25 @@ Dictionary> GetSubsetFonts(List fontInfos, Di _logger?.ZLogDebug($"开始对字体文件信息与 ass 定义的字体进行匹配"); fontMap = []; List matchedAssFontInfos = []; - foreach (FontInfo fontInfo in fontInfos) + + var fiGroups = fontInfos.GroupBy(fontInfo => fontInfo.FamilyName); + foreach (var fig in fiGroups) { - foreach (var assFont in assFonts) + foreach (var afi in assFonts.Keys) { - if (!matchedAssFontInfos.Contains(assFont.Key) && AssFont.IsMatch(assFont.Key, fontInfo)) - { - if (!fontMap.TryGetValue(fontInfo, out var _)) - { - fontMap.Add(fontInfo, []); - } - fontMap[fontInfo].Add(assFont.Key); + if (matchedAssFontInfos.Contains(afi)) { continue; } + var _fontInfo = AssFont.GetMatchedFontInfo(afi, fig, _logger); + if (_fontInfo == null) { continue; } + var fontInfo = (FontInfo) _fontInfo; - matchedAssFontInfos.Add(assFont.Key); - _logger?.ZLogDebug($"{assFont.Key.ToString()} 匹配到了 {fontInfo.FileName} 的索引 {fontInfo.Index}"); + if (!fontMap.TryGetValue(fontInfo, out var _)) + { + fontMap.Add(fontInfo, []); } + fontMap[fontInfo].Add(afi); + + matchedAssFontInfos.Add(afi); + _logger?.ZLogDebug($"{afi.ToString()} 匹配到了 {fontInfo.FileName} 的索引 {fontInfo.Index}"); } } _logger?.ZLogDebug($"匹配完成"); diff --git a/AssFontSubset.CoreTests/src/AssFontTests.cs b/AssFontSubset.CoreTests/src/AssFontTests.cs index 275866f..0f9be64 100644 --- a/AssFontSubset.CoreTests/src/AssFontTests.cs +++ b/AssFontSubset.CoreTests/src/AssFontTests.cs @@ -7,47 +7,42 @@ namespace AssFontSubset.Core.Tests; public class AssFontTests { [TestMethod] - public void IsMatchTestTrueBIZ() + public void MatchTestTrueBIZ() { var fn = "Times New Roman"; var fnChs = "Times New Roman"; - var afi = new AssFontInfo() { Name = fn, Weight = 0, Italic = false }; + var afiR = new AssFontInfo() { Name = fn, Weight = 0, Italic = false }; var afiB = new AssFontInfo() { Name = fn, Weight = 1, Italic = false }; var afiI = new AssFontInfo() { Name = fn, Weight = 0, Italic = true }; var afiZ = new AssFontInfo() { Name = fn, Weight = 1, Italic = true }; var afi4 = new AssFontInfo() { Name = fn, Weight = 400, Italic = false }; - var fi = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = false, MaybeHasTrueBoldOrItalic = true, Weight = 400 }; - var fiB = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = false, MaybeHasTrueBoldOrItalic = false, Weight = 700 }; - var fiI = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = true, MaybeHasTrueBoldOrItalic = false, Weight = 400 }; - var fiZ = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = true, MaybeHasTrueBoldOrItalic = false, Weight = 700 }; + var fiR = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = false, Weight = 400 }; + var fiB = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = false, Weight = 700 }; + var fiI = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = true, Weight = 400 }; + var fiZ = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = true, Weight = 700 }; + + var afL = new List() { afiR, afiB, afiI, afiZ, afi4 }; + var fiL = new List() { fiR, fiB, fiI, fiZ }; + var fiGroups = fiL.GroupBy(fontInfo => fontInfo.FamilyName); - var afL = new List() { afi, afiB, afiI, afiZ, afi4 }; - var fiL = new List() { fi, fiB, fiI, fiZ }; foreach (var a in afL) { - foreach (var f in fiL) + foreach (var f in fiGroups) { - if ((a == afi && f == fi) - || (a == afiB && f == fiB) - || (a == afiI && f == fiI) - || (a == afiZ && f == fiZ) - || (a == afi4 && f == fi) - ) - { - Assert.IsTrue(IsMatch(a, f)); - } - else - { - Assert.IsFalse(IsMatch(a, f)); - } + var tfi = GetMatchedFontInfo(a, f); + if (a == afiR) { Assert.IsTrue(fiR == tfi); } + if (a == afiB) { Assert.IsTrue(fiB == tfi); } + if (a == afiI) { Assert.IsTrue(fiI == tfi); } + if (a == afiZ) { Assert.IsTrue(fiZ == tfi); } + if (a == afi4) { Assert.IsTrue(fiR == tfi); } } } } [TestMethod] - public void IsMatchTestFakeBIZ() + public void MatchTestFakeBIZ() { var fn = "FZLanTingHei-R-GBK"; var fnChs = "方正兰亭黑_GBK"; @@ -61,12 +56,80 @@ public void IsMatchTestFakeBIZ() var afiIFChs = new AssFontInfo() { Name = fnChs, Weight = 0, Italic = true }; var afiZFChs = new AssFontInfo() { Name = fnChs, Weight = 1, Italic = true }; - var fi = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = false, MaybeHasTrueBoldOrItalic = false, Weight = 400 }; + var fi = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = false, Weight = 400 }; var afL = new List() { afi, afiBF, afiIF, afiZF, afiChs, afiBFChs, afiIFChs, afiZFChs }; foreach ( var af in afL ) { - Assert.IsTrue(IsMatch(af, fi)); + Assert.IsTrue(IsMatch(af, fi, true)); + } + } + + [TestMethod] + public void MatchTestPartiallyTrueBIZ() + { + var fn = "Times New Roman"; + var fnChs = "Times New Roman"; + + var afiR = new AssFontInfo() { Name = fn, Weight = 0, Italic = false }; + var afiB = new AssFontInfo() { Name = fn, Weight = 1, Italic = false }; + var afiI = new AssFontInfo() { Name = fn, Weight = 0, Italic = true }; + var afiZ = new AssFontInfo() { Name = fn, Weight = 1, Italic = true }; + var afi4 = new AssFontInfo() { Name = fn, Weight = 400, Italic = false }; + + //var fiR = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = false, Weight = 400 }; + //var fiB = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = false, Weight = 700 }; + var fiI = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = true, Weight = 400 }; + var fiZ = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = true, Weight = 700 }; + + var afL = new List() { afiR, afiB, afiI, afiZ, afi4 }; + var fiL = new List() { fiI, fiZ }; + var fiGroups = fiL.GroupBy(fontInfo => fontInfo.FamilyName); + + foreach (var a in afL) + { + foreach (var f in fiGroups) + { + var tfi = GetMatchedFontInfo(a, f); + if (a == afiR) { Assert.IsTrue(null == tfi); } + if (a == afiB) { Assert.IsTrue(null == tfi); } + if (a == afiI) { Assert.IsTrue(fiI == tfi); } + if (a == afiZ) { Assert.IsTrue(fiZ == tfi); } + if (a == afi4) { Assert.IsTrue(null == tfi); } + } + } + } + + [TestMethod] + public void MatchTestTrueB() + { + var fn = "Source Han Sans"; + var fnChs = "思源黑体"; + + var afiR = new AssFontInfo() { Name = fn, Weight = 0, Italic = false }; + var afiB = new AssFontInfo() { Name = fn, Weight = 1, Italic = false }; + var afiI = new AssFontInfo() { Name = fn, Weight = 0, Italic = true }; + var afiZ = new AssFontInfo() { Name = fn, Weight = 1, Italic = true }; + var afi4 = new AssFontInfo() { Name = fn, Weight = 400, Italic = false }; + + var fiR = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = false, Italic = false, Weight = 400, MaxpNumGlyphs = 65535 }; + var fiB = new FontInfo() { FamilyName = fn, FamilyNameChs = fnChs, Bold = true, Italic = false, Weight = 700, MaxpNumGlyphs = 65535 }; + + var afL = new List() { afiR, afiB, afiI, afiZ, afi4 }; + var fiL = new List() { fiR, fiB }; + var fiGroups = fiL.GroupBy(fontInfo => fontInfo.FamilyName); + + foreach (var a in afL) + { + foreach (var f in fiGroups) + { + var tfi = GetMatchedFontInfo(a, f); + if (a == afiR) { Assert.IsTrue(fiR == tfi); } + if (a == afiB) { Assert.IsTrue(fiB == tfi); } + if (a == afiI) { Assert.IsTrue(fiR == tfi); } + if (a == afiZ) { Assert.IsTrue(fiB == tfi); } + if (a == afi4) { Assert.IsTrue(fiR == tfi); } + } } } } \ No newline at end of file