Есть ли способ программно определить, есть ли у файла шрифта определенный символ Unicode? - PullRequest
31 голосов
/ 19 сентября 2008

Я работаю над проектом, который генерирует PDF-файлы, которые могут содержать довольно сложные математические и научные формулы. Текст представлен в Times New Roman, который имеет довольно хорошее покрытие Unicode, но не полный. У нас есть система, позволяющая заменить более полный шрифт Unicode для кодовых точек, которые не имеют глифа в TNR (как большинство математических символов «незнакомца»), но я не могу найти способ сделать запрос файл * .ttf, чтобы увидеть, присутствует ли данный глиф. Пока что я просто жестко запрограммировал таблицу поиска, в которой присутствуют кодовые точки, но я бы предпочел автоматическое решение.

Я использую VB.Net в веб-системе под ASP.net, но приветствуются решения на любом языке / среде программирования.

Edit: решение win32 выглядит превосходно, но конкретный случай, который я пытаюсь решить, находится в веб-системе ASP.Net. Есть ли способ сделать это без включения библиотеки Windows API на моем веб-сайте?

Ответы [ 6 ]

10 голосов
/ 19 сентября 2008

Вот проход, используя c # и api windows.

[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;
}

Затем, учитывая символ 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  
1 голос
/ 04 июля 2012

Скотт ответит хорошо. Вот другой подход, который, вероятно, быстрее, если проверять только пару строк на шрифт (в нашем случае 1 строка на шрифт). Но, вероятно, медленнее, если вы используете один шрифт для проверки тонны текста.

    [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;
    }
1 голос
/ 19 сентября 2008

FreeType - это библиотека, которая может читать файлы шрифтов TrueType (среди прочих) и может использоваться для запроса шрифта для конкретного глифа. Однако FreeType предназначен для рендеринга, поэтому его использование может привести к тому, что вы получите больше кода, чем нужно для этого решения.

К сожалению, на самом деле нет четкого решения даже в мире шрифтов OpenType / TrueType; отображение символа на глиф имеет около дюжины различных определений в зависимости от типа шрифта и платформы, для которой он был изначально разработан. Вы можете попытаться взглянуть на определение таблицы cmap в копии Microsoft OpenType spec , но это не совсем легко прочитать.

0 голосов
/ 26 марта 2019

Я сделал это только с помощью модульного теста VB.Net и без вызовов API WIN32. Он включает в себя код для проверки конкретных символов 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 &= $",[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

Вывод был

M N = Консолас, gtf = Консолас, [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 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 голосов
/ 14 августа 2009

Код, опубликованный Скоттом Николсом, великолепен, за исключением одной ошибки: если идентификатор глифа больше 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

А затем изменил основной цикл for в функции GetUnicodeRangesForFont, чтобы он выглядел следующим образом:

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 голосов
/ 19 сентября 2008

Эта статья Microsoft KB может помочь: http://support.microsoft.com/kb/241020

Он немного устарел (изначально был написан для Windows 95), но общий принцип все еще может применяться. Примером кода является C ++, но, поскольку он просто вызывает стандартные API-интерфейсы Windows, он, скорее всего, будет работать и на языках .NET с небольшим отводом смазки.

-Edit- Кажется, что старые API эпохи 95-х годов устарели из-за нового API, который Microsoft называет « Uniscribe », который должен быть в состоянии делать то, что вам нужно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...