#if !WITHOUT_UNISCRIBE using FastReport.Fonts; using FastReport.Utils; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace FastReport.Export.TTF { internal partial class ExportTTFFont { #region DLL import [DllImport("Gdi32.dll")] private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); [DllImport("Gdi32.dll")] private static extern IntPtr DeleteObject(IntPtr hgdiobj); [DllImport("Gdi32.dll")] private static extern int GetOutlineTextMetrics(IntPtr hdc, int cbData, ref TrueTypeFont.OutlineTextMetric lpOTM); [DllImport("Gdi32.dll", CharSet = CharSet.Auto)] private static extern uint GetFontData(IntPtr hdc, uint dwTable, uint dwOffset, [In, Out] IntPtr lpvBuffer, uint cbData); [DllImport("Gdi32.dll")] private static extern int GetCharWidthI(IntPtr hdc, int giFirst, int cgi, [MarshalAs(UnmanagedType.LPArray)] ushort[] pgi, [Out, MarshalAs(UnmanagedType.LPArray)] int[] piWidths); [DllImport("usp10.dll")] private static extern int ScriptFreeCache(ref IntPtr psc); [DllImport("usp10.dll")] private static extern int ScriptIsComplex( [MarshalAs(UnmanagedType.LPWStr)] string pwcInChars, int cInChars, int dwFlags); [DllImport("usp10.dll")] private static extern int ScriptItemize( [MarshalAs(UnmanagedType.LPWStr)] string pwcInChars, int cInChars, int cMaxItems, ref SCRIPT_CONTROL psControl, ref SCRIPT_STATE psState, [In, Out] SCRIPT_ITEM[] pItems, ref int pcItems); [DllImport("usp10.dll")] private static extern int ScriptLayout( int cRuns, [MarshalAs(UnmanagedType.LPArray)] byte[] pbLevel, [MarshalAs(UnmanagedType.LPArray)] int[] piVisualToLogical, [MarshalAs(UnmanagedType.LPArray)] int[] piLogicalToVisual); [DllImport("usp10.dll")] private static extern int ScriptShape( IntPtr hdc, ref IntPtr psc, [MarshalAs(UnmanagedType.LPWStr)] string pwcChars, int cChars, int cMaxGlyphs, ref SCRIPT_ANALYSIS psa, [Out, MarshalAs(UnmanagedType.LPArray)] ushort[] pwOutGlyphs, [Out, MarshalAs(UnmanagedType.LPArray)] ushort[] pwLogClust, [Out, MarshalAs(UnmanagedType.LPArray)] SCRIPT_VISATTR[] psva, ref int pcGlyphs); [DllImport("usp10.dll")] private static extern int ScriptPlace( IntPtr hdc, ref IntPtr psc, [MarshalAs(UnmanagedType.LPArray)] ushort[] pwGlyphs, int cGlyphs, [MarshalAs(UnmanagedType.LPArray)] SCRIPT_VISATTR[] psva, ref SCRIPT_ANALYSIS psa, [MarshalAs(UnmanagedType.LPArray)] int[] piAdvance, [Out, MarshalAs(UnmanagedType.LPArray)] GOFFSET[] pGoffset, ref ABC pABC); [DllImport("usp10.dll")] private static extern uint ScriptRecordDigitSubstitution(uint lcid, ref SCRIPT_DIGITSUBSTITUTE psds); [DllImport("usp10.dll")] private static extern int ScriptApplyDigitSubstitution( ref SCRIPT_DIGITSUBSTITUTE psds, ref SCRIPT_CONTROL psc, ref SCRIPT_STATE pss); #endregion #region Uniscribe Structures [StructLayout(LayoutKind.Sequential)] public struct SCRIPT_STATE { public short data; public int uBidiLevel { get { return data & 0x001F; } } public void SetRtl() { data = 0x801; } } [StructLayout(LayoutKind.Sequential)] public struct SCRIPT_ANALYSIS { public short data; public SCRIPT_STATE state; } [StructLayout(LayoutKind.Sequential)] public struct SCRIPT_CONTROL { public int data; } [StructLayout(LayoutKind.Sequential)] public struct SCRIPT_DIGITSUBSTITUTE { public short NationalDigitLanguage; public short TraditionalDigitLanguage; public byte DigitSubstitute; public int dwReserved; } [StructLayout(LayoutKind.Sequential)] public struct SCRIPT_ITEM { public int iCharPos; public SCRIPT_ANALYSIS analysis; } [StructLayout(LayoutKind.Sequential)] public struct SCRIPT_VISATTR { // WORD uJustification : 4; // WORD fClusterStart : 1; // WORD fDiacritic : 1; // WORD fZeroWidth : 1; // WORD fReserved : 1; // WORD fShapeReserved : 8; public ushort data; public ushort uJustification { get { return (ushort)(data & 15u); } } public bool fClusterStart { get { return (data & 16u) == 16u; } } public bool fDiacritic { get { return (data & 32u) == 32u; } } public bool fZeroWidth { get { return (data & 64u) == 64u; } } public bool fReserved { get { return (data & 128u) == 128u; } } public byte fShapeReserved { get { return (byte)((data & 65280u) / 256); } } } [StructLayout(LayoutKind.Sequential)] public struct GOFFSET { public int du; public int dv; } [StructLayout(LayoutKind.Sequential)] public struct ABC { public int abcA; public int abcB; public int abcC; } #endregion #region EMF structures public const int LF_FACESIZE = 32; public const int LF_FULLFACESIZE = 64; public const int ELF_VENDOR_SIZE = 4; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct EMREXTCREATEFONTINDIRECT { public int ihFont; // Font handle index public EXTLOGFONT elfw; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct EXTLOGFONT { public LOGFONT elfLogFont; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = LF_FULLFACESIZE)] public string elfFullName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = LF_FACESIZE)] public string elfStyle; public int elfVersion; // 0 for the first release of NT public int elfStyleSize; public int elfMatch; public int elfReserved; [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I1, SizeConst = ELF_VENDOR_SIZE)] public byte[] elfVendorId; public int elfCulture; // 0 for Latin public PANOSE elfPanose; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Size = 92)] public struct LOGFONT { public int lfHeight; public int lfWidth; public int lfEscapement; public int lfOrientation; public int lfWeight; public byte lfItalic; public byte lfUnderline; public byte lfStrikeOut; public byte lfCharSet; public byte lfOutPrecision; public byte lfClipPrecision; public byte lfQuality; public byte lfPitchAndFamily; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = LF_FACESIZE)] public string lfFaceName; } [StructLayout(LayoutKind.Sequential)] public struct PANOSE { public byte bFamilyType; public byte bSerifStyle; public byte bWeight; public byte bProportion; public byte bContrast; public byte bStrokeVariation; public byte bArmStyle; public byte bLetterform; public byte bMidline; public byte bXHeight; } #endregion #region Private variables private Cache cache = new Cache(); private SCRIPT_DIGITSUBSTITUTE digitSubstitute; #endregion #region Private methods public void FillOutlineTextMetrix() { if (sourceFont != null) { using (Graphics g = Graphics.FromImage(tempBitmap)) { IntPtr hdc = g.GetHdc(); IntPtr f = sourceFont.ToHfont(); try { SelectObject(hdc, f); GetOutlineTextMetrics(hdc, Marshal.SizeOf(typeof(TrueTypeFont.OutlineTextMetric)), ref textMetric); } finally { DeleteObject(f); g.ReleaseHdc(hdc); } } } } internal static void GDI32_GetFontData(Font font, out IntPtr font_data, out FontType CollectionMode, out uint fontDataSize) { font_data = IntPtr.Zero; Bitmap tempBitmap = new Bitmap(1, 1); using (Graphics g = Graphics.FromImage(tempBitmap)) { IntPtr hdc = g.GetHdc(); IntPtr f = font.ToHfont(); SelectObject(hdc, f); try { // Try to read TrueTypeCollection CollectionMode = FontType.TrueTypeCollection; fontDataSize = GetFontData(hdc, (uint)CollectionMode, 0, IntPtr.Zero, 0); if (fontDataSize == uint.MaxValue) { CollectionMode = FontType.TrueTypeFont; fontDataSize = GetFontData(hdc, (uint)CollectionMode, 0, IntPtr.Zero, 0); } font_data = Marshal.AllocHGlobal((int)fontDataSize); GetFontData(hdc, (uint)CollectionMode, 0, font_data, fontDataSize); } finally { DeleteObject(f); g.ReleaseHdc(hdc); tempBitmap.Dispose(); } } } internal static FontStream GetFontStream(Font source_font) { MemoryStream mem_stream; IntPtr font_image; FontType mode; uint size; GDI32_GetFontData(source_font, out font_image, out mode, out size); byte[] buffer = new byte[size]; Marshal.Copy(font_image, buffer, 0, buffer.Length); mem_stream = new MemoryStream(buffer); buffer = null; return new FontStream(mem_stream); } private Font[] GetFallbackFonts(IntPtr hdc, Font originalFont, string text) { // sad but true. To obtain a fallback font, we draw a string on a metafile, then analyse its records. // https://chromium.googlesource.com/chromium/src/+/22aed04422b04b2cf04f7b7d61392da4e9a2c85a/ui/gfx/font_fallback_win.cc Metafile mf = new Metafile(hdc, EmfType.EmfOnly); using (Graphics g = Graphics.FromImage(mf)) { g.DrawString(text, originalFont, Brushes.Black, 0, 0); } List fallbackFonts = new List(); using (Graphics g = Graphics.FromHdc(hdc)) { g.EnumerateMetafile(mf, Point.Empty, (EmfPlusRecordType recordType, int flags, int dataSize, IntPtr data, PlayRecordCallback callbackData) => { if (recordType == EmfPlusRecordType.EmfExtCreateFontIndirect) { EMREXTCREATEFONTINDIRECT rec = (EMREXTCREATEFONTINDIRECT)Marshal.PtrToStructure(data, typeof(EMREXTCREATEFONTINDIRECT)); fallbackFonts.Add(Font.FromLogFont(rec.elfw.elfLogFont)); } return true; }); } return fallbackFonts.ToArray(); } private RunInfo GetRunInfo(IntPtr hdc, Run run, Font originalFont, Font measureFont) { // initialize structures int maxGlyphs = run.text.Length * 3; SCRIPT_ANALYSIS psa = run.analysis; ushort[] clusters = new ushort[maxGlyphs]; SCRIPT_VISATTR[] psva = new SCRIPT_VISATTR[maxGlyphs]; GOFFSET[] gOffset = new GOFFSET[maxGlyphs]; ABC abc = new ABC(); ushort[] glyphs = new ushort[maxGlyphs]; int[] widths = new int[maxGlyphs]; int numGlyphs = 0; int fallbackIndex = -1; bool hasEmptyGlyphs = false; Font font = originalFont; Font[] fallbackFonts = null; var cacheItem = cache.GetCacheItem(hdc, measureFont); while (true) { // make glyphs ScriptShape(hdc, ref cacheItem.usCache, run.text, run.text.Length, glyphs.Length, ref psa, glyphs, clusters, psva, ref numGlyphs); // check missing glyphs hasEmptyGlyphs = false; if (numGlyphs == 0) hasEmptyGlyphs = true; else { for (int i = 0; i < numGlyphs; i++) { if (glyphs[i] == 0) { hasEmptyGlyphs = true; break; } } } if (!hasEmptyGlyphs) break; else { // first call - generate fallback fonts list if (fallbackIndex == -1) { // obtain fallback fonts. Use sourceFont instead of originalFont as a source (because of round errors in case of small fonts). // sourceFont is 750pt size. fallbackFonts = GetFallbackFonts(hdc, sourceFont, run.text); } fallbackIndex++; if (fallbackIndex >= fallbackFonts.Length) break; Font fallbackFont = fallbackFonts[fallbackIndex]; // fallback font size may not be the same as original (case: japanese fonts) float fallbackSizeRatio = fallbackFont.SizeInPoints / sourceFont.SizeInPoints; // measureFont must be the same size as sourceFont (750pt) measureFont = new Font(fallbackFont.FontFamily, sourceFont.Size, sourceFont.Style); // finally, the font that will be used in runInfo font = new Font(fallbackFont.FontFamily, originalFont.Size * fallbackSizeRatio, originalFont.Style); cacheItem = cache.GetCacheItem(hdc, measureFont); } } // make widths ScriptPlace(hdc, ref cacheItem.usCache, glyphs, numGlyphs, psva, ref psa, widths, gOffset, ref abc); RunInfo runInfo = new RunInfo(numGlyphs) { Font = font }; if (numGlyphs > 0) { // make ToUnicode for (int i = 0; i < run.text.Length; i++) { int glyphIndex = clusters[i]; if (String.IsNullOrEmpty(runInfo.GlyphToUnicode[glyphIndex])) runInfo.GlyphToUnicode[glyphIndex] = ""; runInfo.GlyphToUnicode[glyphIndex] += run.text[i]; } } // get glyph widths (non-shaped) and use it instead advance widths provided by ScriptPlace // The reason is that gdi+ does not use kerning. Widths returned by ScriptPlace may vary, // we need constant glyph width to set up pdf Widths array int[] gwidths = font == originalFont ? GetGlyphWidths(hdc, numGlyphs, glyphs) : // no font fallback, use faster method GetGlyphWidthsSlow(hdc, numGlyphs, glyphs); for (int i = 0; i < numGlyphs; i++) { runInfo.Glyphs[i] = glyphs[i]; runInfo.Widths[i] = gwidths[i]; // we may get kerning like this, but it will result in wrong alignment of right- or center-aligned text because gdi+ draws/measures text w/o kerning //runInfo.Kernings[i] = widths[i] - gwidths[i]; // the only kerning we use is one for complex script's special symbols // works but requires width correction /*runInfo.Kernings[i] = -gOffset[i].du + widths[i] - gwidths[i]; if (i > 0) { runInfo.Kernings[i - 1] += gOffset[i].du; } else { runInfo.ZeroOffsetX = gOffset[i].du * font.Size / sourceFont.Size; } // this should be converted to objfont units runInfo.VerticalOffsets[i] = -gOffset[i].dv * font.Size / sourceFont.Size;*/ // works if (gOffset[i].du != 0 || gOffset[i].dv != 0) { if (i > 0) { if (gOffset[i - 1].du != 0) runInfo.Kernings[i - 1] += gOffset[i].du; // do not kern twice (case: Calibri + arabic) else runInfo.Kernings[i - 1] = widths[i - 1] - gwidths[i - 1] + gOffset[i].du; } else { runInfo.ZeroOffsetX = gOffset[i].du * font.Size / sourceFont.Size; } runInfo.Kernings[i] = widths[i] - gwidths[i] - gOffset[i].du; // this should be converted to objfont units runInfo.VerticalOffsets[i] = -gOffset[i].dv * font.Size / sourceFont.Size; } } return runInfo; } private RunInfo GetRunInfoNonComplex(string text, Font originalFont) { int numGlyphs; ushort[] glyphs; float[] widths; float[] kernings; using (var ttfState = new TTFState(sourceFont)) { numGlyphs = ttfState.Value.GetGlyphIndices(text, 1000, out glyphs, out widths, out kernings, false); } RunInfo runInfo = new RunInfo(numGlyphs) { Font = originalFont }; for (int i = 0; i < numGlyphs; i++) { runInfo.Glyphs[i] = glyphs[i]; runInfo.Widths[i] = (float)Math.Round(widths[i]); runInfo.GlyphToUnicode[i] = text[i].ToString(); } return runInfo; } private int[] GetGlyphWidthsSlow(IntPtr hdc, int numGlyphs, ushort[] glyphs) { int[] gwidths = new int[numGlyphs]; // this api is slow, avoid using it at any cost GetCharWidthI(hdc, 0, numGlyphs, glyphs, gwidths); return gwidths; } private int[] GetGlyphWidths(IntPtr hdc, int numGlyphs, ushort[] glyphs) { int[] glyphWidths = new int[numGlyphs]; for (int i = 0; i < numGlyphs; i++) { ushort glyph = glyphs[i]; if (usedGlyphChars.TryGetValue(glyph, out GlyphChar glyphChar)) { // get existing glyph width glyphWidths[i] = (int)(glyphChar.Width); } else { // use slow api to get single glyph width. // Note that it is better to get all missing glyphs at a time; but in real word this won't be any faster glyphWidths[i] = GetGlyphWidthsSlow(hdc, 1, new ushort[] { glyph })[0]; } } return glyphWidths; } private List Itemize(string s, bool rtl) { int maxItems = s.Length * 2; SCRIPT_ITEM[] pItems = new SCRIPT_ITEM[maxItems]; int pcItems = 0; // initialize Control and State SCRIPT_CONTROL control = new SCRIPT_CONTROL(); SCRIPT_STATE state = new SCRIPT_STATE(); if (rtl) { // this is needed to start paragraph from right state.SetRtl(); // to substitute arabic digits ScriptApplyDigitSubstitution(ref digitSubstitute, ref control, ref state); } // itemize ScriptItemize(s, s.Length, pItems.Length, ref control, ref state, pItems, ref pcItems); // create Run list. Note that ScriptItemize actually returns pcItems+1 items, // so this can be used to calculate char range easily List list = new List(); for (int i = 0; i < pcItems; i++) { string text = s.Substring(pItems[i].iCharPos, pItems[i + 1].iCharPos - pItems[i].iCharPos); list.Add(new Run(text, pItems[i].analysis)); } return list; } private List Layout(List runs) { byte[] pbLevel = new byte[runs.Count]; int[] piVisualToLogical = new int[runs.Count]; // build the pbLevel array for (int i = 0; i < runs.Count; i++) pbLevel[i] = (byte)runs[i].analysis.state.uBidiLevel; // layout runs ScriptLayout(runs.Count, pbLevel, piVisualToLogical, null); // return runs in their visual order List visualRuns = new List(); for (int i = 0; i < piVisualToLogical.Length; i++) visualRuns.Add(runs[piVisualToLogical[i]]); return visualRuns; } private List LayoutString(string str, bool rtl, Font originalFont) { List result = new List(); bool isComplex = ScriptIsComplex(str, str.Length, 1 /*SIC_COMPLEX*/) != 1; if (!isComplex) { RunInfo runInfo = GetRunInfoNonComplex(str, originalFont); result.Add(runInfo); return result; } using (Bitmap b = new Bitmap(1, 1)) { using (Graphics g = Graphics.FromImage(b)) { IntPtr hdc = g.GetHdc(); try { List runs = Itemize(str, rtl); runs = Layout(runs); float offsetX = 0; foreach (Run run in runs) { RunInfo runInfo = GetRunInfo(hdc, run, originalFont, sourceFont); offsetX += runInfo.ZeroOffsetX; runInfo.OffsetX = offsetX; offsetX += (runInfo.Widths.Sum() + runInfo.Kernings.Sum()) * runInfo.Font.Size / sourceFont.Size; result.Add(runInfo); } return result; } finally { g.ReleaseHdc(hdc); } } } } private void InternalInit() { dpiFX = 96f / DrawUtils.ScreenDpi; digitSubstitute = new SCRIPT_DIGITSUBSTITUTE(); ScriptRecordDigitSubstitution(0x0400, ref digitSubstitute); } private void InternalDispose() { cache.Clear(); } #endregion #region Private classes private class Run { public SCRIPT_ANALYSIS analysis; public string text; public Run(string text, SCRIPT_ANALYSIS a) { this.text = text; this.analysis = a; } } #if !CACHED_POINTERS private class Cache { private Dictionary cache = new Dictionary(); public CacheItem GetCacheItem(IntPtr hdc, Font f) { lock (cache) { CacheItem cacheItem; if (!cache.TryGetValue(f, out cacheItem)) { cacheItem = new CacheItem() { hFont = f.ToHfont(), usCache = IntPtr.Zero }; cache.Add(f, cacheItem); } SelectObject(hdc, cacheItem.hFont); return cacheItem; } } public void Clear() { var values = cache.Values.ToArray(); for (int i = 0; i < values.Length; i++) { var item = values[i]; ScriptFreeCache(ref item.usCache); DeleteObject(item.hFont); } cache.Clear(); } } private class CacheItem { public IntPtr hFont; public IntPtr usCache; } #endif #endregion } } #endif