找回密码
 立即注册
楼主: tonyhsie

ListAssFonts: 小工具,分析字幕使用的字型 (2026/1/1 更新)

49

主题

531

回帖

1万

VC币

星辰大海

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
2209108
tmdtmdtmdqq 发表于 2023-7-28 21:47:53 | 显示全部楼层
本帖最后由 tmdtmdtmdqq 于 2023-7-28 21:49 编辑

发现很多Fontworks的字体(遇到过好几次了),我在字幕中用Full font name/Postscript name的值做字体名,ListAssFonts并不认。而FontLoaderSub是可以识别且正常加载的。



full font name.png 不吃full font name.png FLS.png


回复

使用道具 举报

69

主题

1438

回帖

1万

VC币

星辰大海

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
3161197

卓越贡献

tonyhsie  楼主| 发表于 2023-7-28 22:43:36 | 显示全部楼层
tmdtmdtmdqq 发表于 2023-7-28 21:47
发现很多Fontworks的字体(遇到过好几次了),我在字幕中用Full font name/Postscript name的值做字体名,L ...


目前只有思源跟 Noto 的字型,ListAssFonts 有支援它們的 PostScript 名稱


其它字型沒有,因為

1. .Net Framework 無法獲取字型的 PostScript 名稱

2. 不是每一個字體的 PostScript 名稱都可以用在字幕檔裡面,有的可以,有的不行

 思源 Noto 因為其高度使用率,所以我才另外 workaround 想辦法支援,其它字型可能無法一一比照辦理


点评

了解!  发表于 2023-7-28 23:44
回复

使用道具 举报

49

主题

531

回帖

1万

VC币

星辰大海

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
2209108
tmdtmdtmdqq 发表于 2023-7-29 08:11:05 | 显示全部楼层
本帖最后由 tmdtmdtmdqq 于 2023-7-29 08:17 编辑
tonyhsie 发表于 2023-7-28 22:43
目前只有思源跟 Noto 的字型,ListAssFonts 有支援它們的 PostScript 名稱

叫GPT研究了一下,发现.net可以调sharpfont库的接口来获取Postscript name

然而sharpfont库是对freetype库做的一个上层封装,使用时候需要再引用freetype库的。
如果只单单是获取Postscript name的话,引两个库有点累赘。
所以不如直接调freetype库。freetype库的API文档在这里


GPT写了个例子,我随手改了改。没.net开发环境,只能用特殊的方法测试了一下。 freetype库的DLL在这里下。
不知道对你有没有用。贴一下:

  1. using System;
  2. using System.Runtime.InteropServices;
  3. using Library = System.IntPtr;
  4. using Face = System.IntPtr;
  5. using Encoding = System.UInt32;
  6. using Error = System.Int32;

  7. class Program
  8. {
  9.     [DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  10.     public static extern Error FT_Init_FreeType(out Library library);

  11.     [DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  12.     public static extern Error FT_New_Face(Library library, string filepath, uint faceIndex, out Face face);

  13.     [DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  14.     public static extern string FT_Get_Postscript_Name(Face face);

  15.     [DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  16.     private static extern Error FT_Select_Charmap(Face face, Encoding encoding);

  17.     [DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  18.     internal static extern uint FT_Get_Sfnt_Name_Count(Face face);

  19.     [DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  20.     internal static extern Error FT_Get_Sfnt_Name(Face face, uint nameIndex, out SfntNameRec aname);

  21.     //[DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  22.     //private static extern Error FT_Done_Face(Face face);

  23.     [DllImport("freetype.dll", CallingConvention = CallingConvention.Cdecl)]
  24.     // Destroy a given FreeType library object and all of its children, including resources, drivers, faces, sizes, etc.
  25.     private static extern Error FT_Done_FreeType(Library library);

  26.     [DllImport("kernel32.dll")]
  27.     public static extern bool FreeLibrary(Library hModule);

  28.     // define from https://freetype.sourceforge.net/freetype2/docs/reference/ft2-base_interface.html#FT_ENC_TAG
  29.     private static uint EncodingCalc(string s)
  30.     {
  31.         return ((uint)s[0] << 24) | ((uint)s[1] << 16) | ((uint)s[2] << 8) | (uint)s[3];
  32.     }

  33.     // define from https://freetype.sourceforge.net/freetype2/docs/reference/ft2-base_interface.html#FT_Encoding
  34.     static Encoding GetEncodingByName(string encodingName)
  35.     {
  36.         switch (encodingName.ToUpper())
  37.         {
  38.             case "NONE": return 0;
  39.             case "MS_SYMBOL": return EncodingCalc("symb");
  40.             case "UNICODE": return EncodingCalc("unic");
  41.             case "MS_SJIS":
  42.             case "SJIS": return EncodingCalc("sjis");
  43.             case "MS_GB2312":
  44.             case "GB2312": return EncodingCalc("gb  ");
  45.             case "MS_BIG5":
  46.             case "BIG5": return EncodingCalc("big5");
  47.             case "MS_WANSUNG":
  48.             case "WANSUNG": return EncodingCalc("wans");
  49.             case "MS_JOHAB":
  50.             case "JOHAB": return EncodingCalc("joha");
  51.             case "ADOBE_STANDARD": return EncodingCalc("ADOB");
  52.             case "ADOBE_EXPERT": return EncodingCalc("ADBE");
  53.             case "ADOBE_CUSTOM": return EncodingCalc("ADBC");
  54.             case "ADOBE_LATIN_1": return EncodingCalc("lat1");
  55.             case "OLD_LATIN_2": return EncodingCalc("lat2");
  56.             case "APPLE_ROMAN": return EncodingCalc("armn");
  57.             default: return 0;
  58.         }
  59.     }

  60.     static void Main()
  61.     {
  62.         // 动态加载Freetype DLL
  63.         Library library;
  64.         Error error = FT_Init_FreeType(out library);
  65.         if (error != 0)
  66.         {
  67.             Console.WriteLine("Failed to initialize Freetype library");
  68.             return;
  69.         }

  70.         // 打开字体文件
  71.         string fontPath = "z:\\FOT-TelopMinProN-D.otf";
  72.         Face face;
  73.         error = FT_New_Face(library, fontPath, 0, out face);
  74.         if (error != 0)
  75.         {
  76.             Console.WriteLine("Failed to open font file");
  77.             return;
  78.         }

  79.         // 选择Unicode编码
  80.         error = FT_Select_Charmap(face, GetEncodingByName("UNICODE"));
  81.         if (error != 0)
  82.         {
  83.             Console.WriteLine("Fail to select UNICODE charmap");
  84.             return;
  85.         }

  86.         // 获取字体文件的Postscript name字段
  87.         string scriptName = FT_Get_Postscript_Name(face);
  88.         Console.WriteLine("Postscript name: {0}", scriptName);

  89.         // 获取字体文件的全部字段(完整)
  90.         uint sfntNameCount = FT_Get_Sfnt_Name_Count(face);
  91.         for (uint i = 0; i < sfntNameCount; i++)
  92.         {
  93.             SfntNameRec rec = new SfntNameRec();
  94.             error = FT_Get_Sfnt_Name(face, i, out rec);
  95.             if (error == 0)
  96.             {
  97.                 SfntName sfntName = new SfntName(rec);
  98.                 // 各类型ID对应字段的含义可以参考: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
  99.                 Console.WriteLine("field value[{0}] - LanguageId: {1}    NameId: {2}    Value: {3}", i, sfntName.LanguageId, sfntName.NameId, sfntName.StringAnsi);
  100.             }
  101.         }
  102.         
  103.         // 释放资源
  104.         //FT_Done_Face(face);
  105.         FT_Done_FreeType(library);

  106.         // 卸载Freetype DLL
  107.         FreeLibrary(library);
  108.     }

  109.     // copy from https://github.com/Robmaister/SharpFont/blob/master/Source/SharpFont/TrueType/Internal/SfntNameRec.cs
  110.     internal struct SfntNameRec
  111.     {
  112.         internal ushort platform_id;
  113.         internal ushort encoding_id;
  114.         internal ushort language_id;
  115.         internal ushort name_id;

  116.         internal IntPtr @string;
  117.         internal uint string_len;
  118.     }

  119.     // copy from https://github.com/Robmaister/SharpFont/blob/master/Source/SharpFont/TrueType/SfntName.cs
  120.     class SfntName
  121.     {
  122.         private SfntNameRec rec;
  123.         internal SfntName(SfntNameRec rec)
  124.         {
  125.             this.rec = rec;
  126.         }

  127.         public ushort PlatformId
  128.         {
  129.             get
  130.             {
  131.                 return rec.platform_id;
  132.             }
  133.         }

  134.         public ushort EncodingId
  135.         {
  136.             get
  137.             {
  138.                 return rec.encoding_id;
  139.             }
  140.         }

  141.         public ushort LanguageId
  142.         {
  143.             get
  144.             {
  145.                 return rec.language_id;
  146.             }
  147.         }

  148.         public ushort NameId
  149.         {
  150.             get
  151.             {
  152.                 return rec.name_id;
  153.             }
  154.         }

  155.         public string String
  156.         {
  157.             get
  158.             {
  159.                 //TODO it may be possible to consolidate all of these properties
  160.                 //if the strings follow some sane structure. Otherwise, leave
  161.                 //them or add more overloads for common encodings like UTF-8.
  162.                 return Marshal.PtrToStringUni(rec.@string, (int) Math.Ceiling(rec.string_len/2.0));
  163.             }
  164.         }

  165.         public string StringAnsi
  166.         {
  167.             get
  168.             {
  169.                 return Marshal.PtrToStringAnsi(rec.@string, (int)rec.string_len);
  170.             }
  171.         }

  172.         public IntPtr StringPtr
  173.         {
  174.             get
  175.             {
  176.                 return rec.@string;
  177.             }
  178.         }
  179.     }
  180. }

复制代码




点评

謝謝你,但目前不太可能為了PostScript name而讓ListAssFonts額外捆綁一個dll  发表于 2023-7-29 10:08
回复

使用道具 举报

49

主题

531

回帖

1万

VC币

星辰大海

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
2209108
tmdtmdtmdqq 发表于 2023-7-29 19:12:07 | 显示全部楼层
调教了一个下午GPT,终于调教出来了。.net原生方法获取Postscript name。支持TTF/OTF/TTC格式。

CFF比较复杂,建议不处理,只判断tag="name"已经足够。
此处也把CFF的判断代码贴一下,虽然用false给短路了,但在某种情况下可能也会有参考价值。

贴代码:
  1. using System;
  2. using System.IO;

  3. namespace TTFParser
  4. {
  5.     class Program
  6.     {
  7.         static void Main(string[] args)
  8.         {
  9.             Console.Write("Input font file path:");
  10.             string filePath = Console.ReadLine();

  11.             // Read the file as binary data
  12.             byte[] fileData = File.ReadAllBytes(filePath);

  13.             if (fileData.Length >= 4 && fileData[0] == 0x74 && fileData[1] == 0x74 && fileData[2] == 0x63 && fileData[3] == 0x66)
  14.             {
  15.                 // TTC file signature: 0x74 0x74 0x63 0x66 (ttcf)

  16.                 // Parse the TTC header
  17.                 int numFonts = ReadUInt32(fileData, 8);
  18.                 int offsetTableOffset = 12;

  19.                 for (int fontIndex = 0; fontIndex < numFonts; fontIndex++)
  20.                 {
  21.                     // Get the offset and length of the specified font
  22.                     int fontOffset = ReadUInt32(fileData, offsetTableOffset + (fontIndex * 4));

  23.                     FontAttributeAnalysis(fileData, fontOffset);
  24.                 }
  25.             }
  26.             else
  27.             {
  28.                 // OTF file signature: 0x4F 0x54 0x54 0x4F (OTTO)
  29.                 // TTF file signature: 0x00 0x01 0x00 0x00
  30.                 FontAttributeAnalysis(fileData, 0);
  31.             }
  32.         }

  33.         static void FontAttributeAnalysis(byte[] fileData, int tableInitOffset)
  34.         {
  35.             // Parse the font header
  36.             int tableCount = ReadUInt16(fileData, tableInitOffset + 4);
  37.             int tableOffset = tableInitOffset + 12;

  38.             string familyName = null;
  39.             string postscriptName = null;

  40.             // Iterate through the tables to find the name table
  41.             for (int i = 0; i < tableCount; i++)
  42.             {
  43.                 int tagOffset = tableOffset + (i * 16);
  44.                 string tag = ReadString(fileData, tagOffset, 4);

  45.                 if (tag == "name")
  46.                 {
  47.                     int nameTableOffset = ReadUInt32(fileData, tagOffset + 8);
  48.                     int nameCount = ReadUInt16(fileData, nameTableOffset + 2);
  49.                     int stringOffset = ReadUInt16(fileData, nameTableOffset + 4);

  50.                     // Iterate through the name records to find family name and postscript name
  51.                     for (int j = 0; j < nameCount; j++)
  52.                     {
  53.                         int nameRecordOffset = nameTableOffset + 6 + (j * 12);

  54.                         int platformID = ReadUInt16(fileData, nameRecordOffset);
  55.                         int encodingID = ReadUInt16(fileData, nameRecordOffset + 2);
  56.                         int languageID = ReadUInt16(fileData, nameRecordOffset + 4);
  57.                         int nameID = ReadUInt16(fileData, nameRecordOffset + 6);
  58.                         int nameLength = ReadUInt16(fileData, nameRecordOffset + 8);
  59.                         int nameOffset = nameTableOffset + stringOffset + ReadUInt16(fileData, nameRecordOffset + 10);

  60.                         // 各类型ID对应字段的含义可以参考: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
  61.                         Console.WriteLine("[DEBUG] platformID={0}    encodingID={1}    languageID={2}    nameID={3}    value={4}",
  62.                             platformID, encodingID, languageID, nameID, ReadString(fileData, nameOffset, nameLength));

  63.                         if (nameID == 1) // Family name ID
  64.                         {
  65.                             if (familyName == null)
  66.                             {
  67.                                 familyName = ReadString(fileData, nameOffset, nameLength);
  68.                             }
  69.                         }
  70.                         else if (nameID == 6) // Postscript name ID
  71.                         {
  72.                             if (postscriptName == null)
  73.                             {
  74.                                 postscriptName = ReadString(fileData, nameOffset, nameLength);
  75.                             }
  76.                         }

  77.                         //if (familyName != null && postscriptName != null)
  78.                         //{
  79.                         //    break;
  80.                         //}
  81.                     }
  82.                     break;
  83.                 }
  84.                 else if (tag == "CFF " && false)  //先用false屏蔽,建议不处理
  85.                 {
  86.                     // CFF比较复杂,建议不处理,只判断tag="name"已经足够。这只是简单的例子,没有全覆盖,上面tag判断的字符串有个空格,要注意
  87.                     // Adobe的CFF规范 https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5176.CFF.pdf
  88.                     // Parse the 'CFF ' table to find the Postscript name.
  89.                     // The CFF table format is complex and not covered in this simple example.
  90.                     // You need to understand the CFF specification to implement this function.
  91.                     // Here is a very simplified example:

  92.                     Console.WriteLine("[DEBUG] CCF running...");
  93.                     int cffTableOffset = ReadUInt32(fileData, tagOffset + 8);
  94.                     // The Name INDEX is located at offset 2 in the CFF table.
  95.                     int nameIndexOffset = cffTableOffset + 2;
  96.                     int nameCount = ReadUInt16(fileData, nameIndexOffset);

  97.                     // The Top DICT INDEX follows the Name INDEX.
  98.                     int dictIndexOffset = nameIndexOffset + 2 + nameCount * 2 + 1;

  99.                     for (int j = 0; j < nameCount; j++)
  100.                     {
  101.                         // Parse the Name INDEX to get the font name.
  102.                         int nameOffset = ReadUInt16(fileData, nameIndexOffset + 2 + j * 2);
  103.                         int nextNameOffset = ReadUInt16(fileData, nameIndexOffset + 2 + (j + 1) * 2);
  104.                         if (cffTableOffset + nameOffset < nextNameOffset - nameOffset)
  105.                         {
  106.                             string fontName = ReadString(fileData, cffTableOffset + nameOffset, nextNameOffset - nameOffset);
  107.                             Console.WriteLine("CCF Font Name: " + fontName);
  108.                         }

  109.                         // Parse the Top DICT INDEX to get the font dictionary.
  110.                         int dictOffset = ReadUInt16(fileData, dictIndexOffset + 2 + j * 2);
  111.                         int nextDictOffset = ReadUInt16(fileData, dictIndexOffset + 2 + (j + 1) * 2);

  112.                         
  113.                         for (int k = dictOffset; k < nextDictOffset; k++)
  114.                         {
  115.                             // The PostScript name is stored in the font dictionary with the operator 12 30.
  116.                             if (fileData[cffTableOffset + k] == 12 && fileData[cffTableOffset + k + 1] == 30)
  117.                             {
  118.                                 // The PostScript name follows the operator and is encoded as a string.
  119.                                 int postscriptNameLength = fileData[cffTableOffset + k + 2];
  120.                                 postscriptName = ReadString(fileData, cffTableOffset + k + 3, postscriptNameLength);
  121.                                 break;
  122.                             }

  123.                             // The Family name is stored in the font dictionary with the operator 12 38.
  124.                             if (fileData[cffTableOffset + k] == 12 && fileData[cffTableOffset + k + 1] == 38)
  125.                             {
  126.                                 // The Family name follows the operator and is encoded as a string.
  127.                                 int familyNameLength = fileData[cffTableOffset + k + 2];
  128.                                 familyName = ReadString(fileData, cffTableOffset + k + 3, familyNameLength);
  129.                                 break;
  130.                             }
  131.                         }
  132.                     }
  133.                 }
  134.             }

  135.             Console.WriteLine("Family Name: " + familyName);
  136.             Console.WriteLine("Postscript Name: " + postscriptName);
  137.             Console.WriteLine("=============================");
  138.         }

  139.         static int ReadUInt16(byte[] data, int offset)
  140.         {
  141.             return (data[offset] << 8) | data[offset + 1];
  142.         }

  143.         static int ReadUInt32(byte[] data, int offset)
  144.         {
  145.             return (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];
  146.         }

  147.         static string ReadString(byte[] data, int offset, int length)
  148.         {
  149.             return System.Text.Encoding.ASCII.GetString(data, offset, length);
  150.         }
  151.     }
  152. }
复制代码






回复

使用道具 举报

69

主题

1438

回帖

1万

VC币

星辰大海

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
3161197

卓越贡献

tonyhsie  楼主| 发表于 2023-7-29 20:08:00 | 显示全部楼层
tmdtmdtmdqq 发表于 2023-7-29 19:12
调教了一个下午GPT,终于调教出来了。.net原生方法获取Postscript name。支持TTF/OTF/TTC格式。

CFF比较 ...

圖片 2023-07-29 19-59-07.png 圖片 2023-07-29 20-01-30.png


很多年前就寫過 ttf parser 了,三年前徹底拿掉這些 code
因為有更好的方式來處理字型檔案,不需要自己動手


btw,我覺得你的研究精神很好,花點時間,你很快就可以寫出一個自己的字型工具
就像我當年寫出 ListAssFonts 一樣

(認真的,不是嘲諷)


回复

使用道具 举报

24

主题

772

回帖

3104

VC币

星辰大海

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

积分
648753
sommio 发表于 2023-7-31 03:43:57 | 显示全部楼层
tmdtmdtmdqq 发表于 2023-7-28 21:47
发现很多Fontworks的字体(遇到过好几次了),我在字幕中用Full font name/Postscript name的值做字体名,L ...

我觉得用「Weight-stretch-style font family model」比较安全,因为是主流的
无论是 Office 还是 aegisub 的字体选择器都使用这个 model,既使用 family

像之前我发帖报错的「思源黑体 Bold (v2.000+)」,实际上官方 OTF 包含这个 Full Name,但 mpv+fontconfig 无法调用它
在此前的版本中它作为 family 存在

回复

使用道具 举报

0

主题

2

回帖

0

VC币

新手上路

Rank: 1

积分
20
Qianshijiang 发表于 2023-8-24 03:56:35 | 显示全部楼层
我的神!
回复

使用道具 举报

2

主题

28

回帖

10

VC币

荣誉会员

Rank: 14Rank: 14Rank: 14Rank: 14

积分
93138
wzdc 发表于 2023-9-9 16:37:04 | 显示全部楼层
本帖最后由 wzdc 于 2023-9-9 16:40 编辑

az

点评

繼續  发表于 2023-9-10 01:31
回复

使用道具 举报

1

主题

6

回帖

0

VC币

注册会员

Rank: 2

积分
612
sdyt6383985 发表于 2023-9-11 23:08:20 | 显示全部楼层
哇!发现好东西!感谢楼主分享!
回复

使用道具 举报

0

主题

1

回帖

0

VC币

新手上路

Rank: 1

积分
17
onezhai 发表于 2023-9-19 19:51:34 | 显示全部楼层
好感动,这么久了还在更新软件。感恩作者!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表