Есть ли способ программно определить, имеет ли файл шрифта определенный Unicode Glyph?
Я работаю над проектом, который генерирует PDF файлы, которые могут содержать довольно сложные математические и научные формулы. Текст представлен в Times New Roman, который имеет довольно хорошее покрытие Unicode, но не полный. У нас есть система для замены более полного шрифта Unicode для кодовых точек, которые не имеют глифа в TNR (как большинство математических символов "незнакомца"), но я не могу найти способ сделать запрос файл *.ttf, чтобы увидеть, присутствует ли данный глиф. Пока что я просто жестко запрограммировал таблицу поиска, в которой присутствуют кодовые точки, но я бы предпочел автоматическое решение.
Я использую VB.Net в веб-системе под ASP.net, но приветствуются решения на любом языке/среде программирования.
Изменение: решение win32 выглядит превосходно, но конкретный случай, который я пытаюсь решить, в веб-системе ASP.Net. Есть ли способ сделать это без включения DLL API Windows в мой веб-сайт?
Ответы
Ответ 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, который вы хотите проверить, и шрифта 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
Ответ 2
Скотт отвечает хорошо. Вот еще один подход, который, вероятно, быстрее, если проверить только пару строк на шрифт (в нашем случае 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;
}
Ответ 3
FreeType - это библиотека, которая может читать файлы шрифтов TrueType (среди прочего) и может использоваться для запроса шрифта для определенного глиф. Тем не менее, FreeType предназначен для рендеринга, поэтому использование этого может привести к тому, что вы получите больше кода, чем требуется для этого решения.
К сожалению, на самом деле не существует четкого решения даже в мире шрифтов OpenType/TrueType; отображение символов в глиф имеет около десятка различных определений в зависимости от типа шрифта и платформы, изначально предназначенной для нее. Вы можете попытаться посмотреть описание таблицы cmap в копии Microsoft Спецификация OpenType, но это не совсем простое чтение.
Ответ 4
Эта статья Microsoft KB может помочь:
http://support.microsoft.com/kb/241020
Он немного устарел (изначально был написан для Windows 95), но общий принцип все равно может применяться. Пример кода - С++, но поскольку он просто вызывает стандартные API Windows, он, скорее всего, будет работать на языках .NET, а также с небольшой консистентной смазкой.
-Edit-
Похоже, что старые API-интерфейсы 95-й эпохи были устарели новым API-интерфейсом Microsoft: " Uniscribe", который должен иметь возможность делать то, что вы вам нужно это сделать.
Ответ 5
Код, отправленный Скоттом Николсом, замечательный, за исключением одной ошибки: если идентификатор глифа больше, чем 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 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
Ответ 6
Я сделал это только с помощью модульного теста 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=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.