有没有办法以编程方式确定字体文件是不是具有特定的 Unicode 字形?

Posted

技术标签:

【中文标题】有没有办法以编程方式确定字体文件是不是具有特定的 Unicode 字形?【英文标题】:Is there a way to programmatically determine if a font file has a specific Unicode Glyph?有没有办法以编程方式确定字体文件是否具有特定的 Unicode 字形? 【发布时间】:2010-09-11 08:23:25 【问题描述】:

我正在开展一个项目,该项目可以生成包含相当复杂的数学和科学公式的 PDF。文本以 Times New Roman 呈现,它具有很好的 Unicode 覆盖率,但并不完整。我们有一个系统可以为在 TNR 中没有字形的代码点(如大多数“陌生”数学符号)交换更 Unicode 完整的字体,但我似乎找不到查询的方法*.ttf 文件以查看是否存在给定的字形。到目前为止,我只是硬编码了一个存在哪些代码点的查找表,但我更喜欢自动解决方案。

我在 ASP.net 下的 Web 系统中使用 VB.Net,但任何编程语言/环境中的解决方案都将不胜感激。

编辑:win32 解决方案看起来很棒,但我试图解决的具体情况是在 ASP.Net Web 系统中。有没有办法在我的网站中不包含 Windows API DLL 的情况下做到这一点?

【问题讨论】:

【参考方案1】:

这是一个使用 c# 和 windows API 的通行证。

[DllImport("gdi32.dll")]
public static extern uint GetFontUnicodeRanges(IntPtr hdc, IntPtr lpgs);

[DllImport("gdi32.dll")]
public extern static IntPtr SelectObject(IntPtr hDC, IntPtr hObject);

public struct FontRange

    public UInt16 Low;
    public UInt16 High;


public List<FontRange> GetUnicodeRangesForFont(Font font)

    Graphics g = Graphics.FromHwnd(IntPtr.Zero);
    IntPtr hdc = g.GetHdc();
    IntPtr hFont = font.ToHfont();
    IntPtr old = SelectObject(hdc, hFont);
    uint size = GetFontUnicodeRanges(hdc, IntPtr.Zero);
    IntPtr glyphSet = Marshal.AllocHGlobal((int)size);
    GetFontUnicodeRanges(hdc, glyphSet);
    List<FontRange> fontRanges = new List<FontRange>();
    int count = Marshal.ReadInt32(glyphSet, 12);
    for (int i = 0; i < count; i++)
    
        FontRange range = new FontRange();
        range.Low = (UInt16)Marshal.ReadInt16(glyphSet, 16 + i * 4);
        range.High = (UInt16)(range.Low + Marshal.ReadInt16(glyphSet, 18 + i * 4) - 1);
        fontRanges.Add(range);
    
    SelectObject(hdc, old);
    Marshal.FreeHGlobal(glyphSet);
    g.ReleaseHdc(hdc);
    g.Dispose();
    return fontRanges;


public bool CheckIfCharInFont(char character, Font font)

    UInt16 intval = Convert.ToUInt16(character);
    List<FontRange> ranges = GetUnicodeRangesForFont(font);
    bool isCharacterPresent = false;
    foreach (FontRange range in ranges)
    
        if (intval >= range.Low && intval <= range.High)
        
            isCharacterPresent = true;
            break;
        
    
    return isCharacterPresent;

然后,给定一个要检查的 char toCheck 和一个 Font theFont 来测试它...

if (!CheckIfCharInFont(toCheck, theFont) 
    // not present

使用 VB.Net 的相同代码

<DllImport("gdi32.dll")> _
Public Shared Function GetFontUnicodeRanges(ByVal hds As IntPtr, ByVal lpgs As IntPtr) As UInteger
End Function  

<DllImport("gdi32.dll")> _
Public Shared Function SelectObject(ByVal hDc As IntPtr, ByVal hObject As IntPtr) As IntPtr
End Function  

Public Structure FontRange
    Public Low As UInt16
    Public High As UInt16
End Structure  

Public Function GetUnicodeRangesForFont(ByVal font As Font) As List(Of FontRange)
    Dim g As Graphics
    Dim hdc, hFont, old, glyphSet As IntPtr
    Dim size As UInteger
    Dim fontRanges As List(Of FontRange)
    Dim count As Integer

    g = Graphics.FromHwnd(IntPtr.Zero)
    hdc = g.GetHdc()
    hFont = font.ToHfont()
    old = SelectObject(hdc, hFont)
    size = GetFontUnicodeRanges(hdc, IntPtr.Zero)
    glyphSet = Marshal.AllocHGlobal(CInt(size))
    GetFontUnicodeRanges(hdc, glyphSet)
    fontRanges = New List(Of FontRange)
    count = Marshal.ReadInt32(glyphSet, 12)

    For i = 0 To count - 1
        Dim range As FontRange = New FontRange
        range.Low = Marshal.ReadInt16(glyphSet, 16 + (i * 4))
        range.High = range.Low + Marshal.ReadInt16(glyphSet, 18 + (i * 4)) - 1
        fontRanges.Add(range)
    Next

    SelectObject(hdc, old)
    Marshal.FreeHGlobal(glyphSet)
    g.ReleaseHdc(hdc)
    g.Dispose()

    Return fontRanges
End Function  

Public Function CheckIfCharInFont(ByVal character As Char, ByVal font As Font) As Boolean
    Dim intval As UInt16 = Convert.ToUInt16(character)
    Dim ranges As List(Of FontRange) = GetUnicodeRangesForFont(font)
    Dim isCharacterPresent As Boolean = False

    For Each range In ranges
        If intval >= range.Low And intval <= range.High Then
            isCharacterPresent = True
            Exit For
        End If
    Next range
    Return isCharacterPresent
End Function  

【讨论】:

如果你想经常调用它,你可能想缓存你获得的范围,也许将它封装在 CharInFontChecker 类或其他类中。 让我感到困扰的是,经过测试的几种字体确实呈现了正确的字形,但返回的不是字体。【参考方案2】:

我只用了一个 VB.Net 单元测试,没有 WIN32 API 调用。它包括检查特定字符 U+2026(省略号)和 U+2409(HTab)的代码,还返回具有字形的字符数(以及低值和高值)。我只对等宽字体感兴趣,但很容易改变......

    Dim fnt As System.Drawing.Font, size_M As Drawing.Size, size_i As Drawing.Size, size_HTab As Drawing.Size, isMonospace As Boolean
    Dim ifc = New Drawing.Text.InstalledFontCollection
    Dim bm As Drawing.Bitmap = New Drawing.Bitmap(640, 64), gr = Drawing.Graphics.FromImage(bm)
    Dim tf As Windows.Media.Typeface, gtf As Windows.Media.GlyphTypeface = Nothing, ok As Boolean, gtfName = ""

    For Each item In ifc.Families
        'TestContext_WriteTimedLine($"N=item.Name.")
        fnt = New Drawing.Font(item.Name, 24.0)
        Assert.IsNotNull(fnt)

        tf = New Windows.Media.Typeface(item.Name)
        Assert.IsNotNull(tf, $"item.Name=item.Name")

        size_M = System.Windows.Forms.TextRenderer.MeasureText("M", fnt)
        size_i = System.Windows.Forms.TextRenderer.MeasureText("i", fnt)
        size_HTab = System.Windows.Forms.TextRenderer.MeasureText(ChrW(&H2409), fnt)
        isMonospace = size_M.Width = size_i.Width
        Assert.AreEqual(size_M.Height, size_i.Height, $"fnt=fnt.Name")

        If isMonospace Then

            gtfName = "-"
            ok = tf.TryGetGlyphTypeface(gtf)
            If ok Then
                Assert.AreEqual(True, ok, $"item.Name=item.Name")
                Assert.IsNotNull(gtf, $"item.Name=item.Name")
                gtfName = $"gtf.FamilyNames(Globalization.CultureInfo.CurrentUICulture)"

                Assert.AreEqual(True, gtf.CharacterToGlyphMap().ContainsKey(AscW("M")), $"item.Name=item.Name")
                Assert.AreEqual(True, gtf.CharacterToGlyphMap().ContainsKey(AscW("i")), $"item.Name=item.Name")

                Dim t = 0, nMin = &HFFFF, nMax = 0
                For n = 0 To &HFFFF
                    If gtf.CharacterToGlyphMap().ContainsKey(n) Then
                        If n < nMin Then nMin = n
                        If n > nMax Then nMax = n
                        t += 1
                    End If
                Next
                gtfName &= $",[xnMin:X-xnMax:X]#t"

                ok = gtf.CharacterToGlyphMap().ContainsKey(AscW(ChrW(&H2409)))
                If ok Then
                    gtfName &= ",U+2409"
                End If
                ok = gtf.CharacterToGlyphMap().ContainsKey(AscW(ChrW(&H2026)))
                If ok Then
                    gtfName &= ",U+2026"
                End If
            End If

            Debug.WriteLine($"IIf(isMonospace, "*M*", "") N=fnt.Name, gtf=gtfName.")
            gr.Clear(Drawing.Color.White)
            gr.DrawString($"MiChrW(&H2409) fnt.Name", fnt, New Drawing.SolidBrush(Drawing.Color.Black), 10, 10)
            bm.Save($"fnt.Name_MiHT.bmp")
        End If
    Next

输出是

M N=Consolas, gtf=Consolas,[x0-xFFFC]#2488,U+2026。

M N=Courier New, gtf=Courier New,[x20-xFFFC]#3177,U+2026。

M N=Lucida 控制台,gtf=Lucida 控制台,[x20-xFB02]#644,U+2026。

M N=Lucida Sans 打字机,gtf=Lucida Sans 打字机,[x20-xF002]#240,U+2026。

M N=MingLiU-ExtB, gtf=MingLiU-ExtB,[x0-x2122]#212.

M N=MingLiU_HKSCS-ExtB, gtf=MingLiU_HKSCS-ExtB,[x0-x2122]#212。

M N=MS Gothic, gtf=MS Gothic,[x0-xFFEE]#15760,U+2026。

M N=NSimSun, gtf=NSimSun,[x20-xFFE5]#28737,U+2026。

M N=OCR A 扩展,gtf=OCR A 扩展,[x20-xF003]#248,U+2026。

M N=SimSun, gtf=SimSun,[x20-xFFE5]#28737,U+2026。

M N=SimSun-ExtB, gtf=SimSun-ExtB,[x20-x7F]#96。

M N=Webdings, gtf=Webdings,[x20-xF0FF]#446.

【讨论】:

【参考方案3】:

斯科特的回答很好。如果每个字体只检查几个字符串(在我们的例子中每个字体一个字符串),这是另一种可能更快的方法。但如果您使用一种字体来检查大量文本,可能会更慢。

    [DllImport("gdi32.dll", EntryPoint = "CreateDC", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CreateDC(string lpszDriver, string lpszDeviceName, string lpszOutput, IntPtr devMode);

    [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
    private static extern bool DeleteDC(IntPtr hdc);

    [DllImport("Gdi32.dll")]
    private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

    [DllImport("Gdi32.dll", CharSet = CharSet.Unicode)]
    private static extern int GetGlyphIndices(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string lpstr, int c,
                                              Int16[] pgi, int fl);

    /// <summary>
    /// Returns true if the passed in string can be displayed using the passed in fontname. It checks the font to 
    /// see if it has glyphs for all the chars in the string.
    /// </summary>
    /// <param name="fontName">The name of the font to check.</param>
    /// <param name="text">The text to check for glyphs of.</param>
    /// <returns></returns>
    public static bool CanDisplayString(string fontName, string text)
    
        try
        
            IntPtr hdc = CreateDC("DISPLAY", null, null, IntPtr.Zero);
            if (hdc != IntPtr.Zero)
            
                using (Font font = new Font(new FontFamily(fontName), 12, FontStyle.Regular, GraphicsUnit.Point))
                
                    SelectObject(hdc, font.ToHfont());
                    int count = text.Length;
                    Int16[] rtcode = new Int16[count];
                    GetGlyphIndices(hdc, text, count, rtcode, 0xffff);
                    DeleteDC(hdc);

                    foreach (Int16 code in rtcode)
                        if (code == 0)
                            return false;
                
            
        
        catch (Exception)
        
            // nada - return true
            Trap.trap();
        
        return true;
    

【讨论】:

传递给GetGlyphIndices0xffff 值是什么?微软似乎没有记录(不再?)——they only mention GGI_MARK_NONEXISTING_GLYPHS which has the value of 0x0001.(这里是 ANSI 版本,但 Unicode 版本在这方面没有区别)。【参考方案4】:

Scott Nichols 发布的代码非常棒,除了一个错误:如果字形 id 大于 Int16.MaxValue,则会引发 OverflowException。为了修复它,我添加了以下功能:

Protected Function Unsign(ByVal Input As Int16) As UInt16
    If Input > -1 Then
        Return CType(Input, UInt16)
    Else
        Return UInt16.MaxValue - (Not Input)
    End If
End Function

然后将函数 GetUnicodeRangesForFont 中的主 for 循环更改为如下所示:

For i As Integer = 0 To count - 1
    Dim range As FontRange = New FontRange
    range.Low = Unsign(Marshal.ReadInt16(glyphSet, 16 + (i * 4)))
    range.High = range.Low + Unsign(Marshal.ReadInt16(glyphSet, 18 + (i * 4)) - 1)
    fontRanges.Add(range)
Next

【讨论】:

【参考方案5】:

这篇 Microsoft 知识库文章可能会有所帮助: http://support.microsoft.com/kb/241020

它有点过时了(最初是为 Windows 95 编写的),但一般原则可能仍然适用。示例代码是 C++,但由于它只是调用标准的 Windows API,它很可能在 .NET 语言中也能工作,而且有点麻烦。

-编辑- 似乎旧的 95 时代 API 已被微软称为“Uniscribe”的新 API 淘汰,它应该能够做你需要的事情。

【讨论】:

相反 - UniScribe 使执行 OP 想要的操作变得更难,因为 UniScribe 旨在使查找字形的过程变得透明。例如,UniScribe 将使用 Font Fallback 来选择实际上确实包含缺失字形的不同字体。【参考方案6】:

FreeType 是一个可以读取 TrueType 字体文件(以及其他)的库,可用于查询特定字形的字体。但是,FreeType 是为渲染而设计的,因此使用它可能会导致您引入比此解决方案所需的更多代码。

不幸的是,即使在 OpenType / TrueType 字体的世界中也没有真正明确的解决方案;字符到字形的映射有十几种不同的定义,具体取决于字体类型和最初设计的平台。您可能会尝试查看 Microsoft 复制的 OpenType spec 中的 cmap table definition,但这并不容易阅读。

【讨论】:

以上是关于有没有办法以编程方式确定字体文件是不是具有特定的 Unicode 字形?的主要内容,如果未能解决你的问题,请参考以下文章

如何以编程方式确定字体是不是为 CFF?

有没有办法以编程方式确定输入日期字段是不是不完整?

以编程方式检测文件夹是不是为云本地文件夹

根据背景颜色确定字体颜色

以编程方式确定 Cisco *** 客户端是不是已连接

以编程方式确定 Excel 文件 (.xls) 是不是包含宏