Is there a way to programmatically determine if a font file has a specific Unicode Glyph?

前端 未结 6 892
花落未央
花落未央 2020-11-27 04:19

I\'m working on a project that generates PDFs that can contain fairly complex math and science formulas. The text is rendered in Times New Roman, which has pretty good Unic

相关标签:
6条回答
  • 2020-11-27 04:55

    I have done this with just a VB.Net Unit Test and no WIN32 API calls. It includes code to check specific characters U+2026 (ellipsis) & U+2409 (HTab), and also returns # of characters (and low and high values) that have glyphs. I was only interested in Monospace fonts, but easy enough to change ...

        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 &= $",[x{nMin:X}-x{nMax: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($"Mi{ChrW(&H2409)} {fnt.Name}", fnt, New Drawing.SolidBrush(Drawing.Color.Black), 10, 10)
                bm.Save($"{fnt.Name}_MiHT.bmp")
            End If
        Next
    

    The output was

    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 Console, gtf=Lucida Console,[x20-xFB02]#644,U+2026.

    M N=Lucida Sans Typewriter, gtf=Lucida Sans Typewriter,[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 Extended, gtf=OCR A Extended,[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.

    0 讨论(0)
  • 2020-11-27 04:57

    This Microsoft KB article may help: http://support.microsoft.com/kb/241020

    It's a bit dated (was originally written for Windows 95), but the general principle may still apply. The sample code is C++, but since it's just calling standard Windows APIs, it'll more than likely work in .NET languages as well with a little elbow grease.

    -Edit- It seems that the old 95-era APIs have been obsoleted by a new API Microsoft calls "Uniscribe", which should be able to do what you need it to.

    0 讨论(0)
  • 2020-11-27 04:59

    Here's a pass at it using c# and the 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;
    }
    

    Then, given a char toCheck that you want to check and a Font theFont to test it against...

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

    Same code using 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  
    
    0 讨论(0)
  • 2020-11-27 05:02

    FreeType is a library that can read TrueType font files (among others) and can be used to query the font for a specific glyph. However, FreeType is designed for rendering, so using it might cause you to pull in more code than you need for this solution.

    Unfortunately, there's not really a clear solution even within the world of OpenType / TrueType fonts; the character-to-glyph mapping has about a dozen different definitions depending on the type of font and what platform it was originally designed for. You might try to look at the cmap table definition in Microsoft's copy of the OpenType spec, but it's not exactly easy reading.

    0 讨论(0)
  • 2020-11-27 05:04

    The code posted by Scott Nichols is great, except for one bug: if the glyph id is greater than Int16.MaxValue, it throws an OverflowException. To fix it, I added the following function:

    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
    

    And then changed the main for loop in the function GetUnicodeRangesForFont to look like this:

    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
    
    0 讨论(0)
  • 2020-11-27 05:13

    Scott's answer is good. Here is another approach that is probably faster if checking just a couple of strings per font (in our case 1 string per font). But probably slower if you are using one font to check a ton of text.

        [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;
        }
    
    0 讨论(0)
提交回复
热议问题