// This idea was borrowed from Google Chrome's uniscribe handler: // https://code.google.com/p/chromium/codesearch#chromium/src/ui/gfx/font_fallback_win.cc&q=uniscribe&sq=package:chromium&l=283&dr=CSs // also as discussed here: // http://stackoverflow.com/questions/16828868/how-to-automatically-choose-most-suitable-font-for-different-language private static bool GetUniscribeFallbackFont( Font font, IntPtr text, int textLength, out GDI.LOGFONT fallbackFont) { fallbackFont = new GDI.LOGFONT(); // Figure out what font Uniscribe chooses for fallback by making it write to a // metafile and then cracking it open. using (GDIDC hdc = GDIDC.CreateCompatibleDC(IntPtr.Zero)) { IntPtr hdcMF_ = GDI.CreateEnhMetaFile(hdc, null, IntPtr.Zero, null); if (hdcMF_ == IntPtr.Zero) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } using (GDIDC hdcMF = new GDIDC(hdcMF_)) { using (GDIFont gdiFont = new GDIFont(font)) { GDI.SelectObject(hdcMF, gdiFont); IntPtr sa = IntPtr.Zero; try { int hr; hr = ScriptStringAnalyse( hdcMF, text, textLength, 0, // cGlyphs -1, // iCharSet SSA_METAFILE | SSA_FALLBACK | SSA_GLYPHS | SSA_LINK, 0, // required width IntPtr.Zero, // SCRIPT_CONTROL IntPtr.Zero, // SCRIPT_STATE null, // piDx IntPtr.Zero, // SCRIPT_TABDEF null, // legacy out sa); if (hr < 0) { return false; } hr = ScriptStringOut( sa, 0, // iX 0, // iY 0, // ExtTextOut options IntPtr.Zero, // rect 0, // iMinSel 0, // iMaxSel false); // fDisabled if (hr < 0) { return false; } } finally { ScriptStringFree(ref sa); } IntPtr hMF = GDI.CloseEnhMetaFile(hdcMF); if (hMF == IntPtr.Zero) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } try { GDI.LOGFONT lf = new GDI.LOGFONT(); bool r = GDI.EnumEnhMetaFile( IntPtr.Zero, // hdc hMF, delegate ( IntPtr hdc_, IntPtr[] lpht,// capacity is nHandles IntPtr lpmr, // ENHMETARECORD int nHandles, int data) // LPARAM { // read lpmr->iType int iType = Marshal.ReadInt32(lpmr); if (iType == GDI.EMR_EXTCREATEFONTINDIRECTW) { object cfio = Marshal.PtrToStructure(lpmr, typeof(GDI.EMREXTCREATEFONTINDIRECT)); GDI.EMREXTCREATEFONTINDIRECT cfi = (GDI.EMREXTCREATEFONTINDIRECT)cfio; // Last one wins! lf = cfi.elfw.elfLogFont; } return 1; }, IntPtr.Zero, // LPARAM IntPtr.Zero); // &RECT if (!r) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } if (!String.IsNullOrEmpty(lf.lfFaceName)) { fallbackFont = lf; return true; } } finally { GDI.DeleteEnhMetaFile(hMF); } return false; } } } }
// This article provides a real-world tutorial of how to actually use the API, including // a discussion of many gotchas not identified in the MSDN documentation. // http://www.catch22.net/tuts/uniscribe-mysteries // http://www.catch22.net/tuts/more-uniscribe-mysteries // http://www.catch22.net/tuts/drawing-styled-text-uniscribe // From the developer of Google Chrome's uniscribe client: // https://maxradi.us/documents/uniscribe/ // Arabic embedded (RTL) example: // http://blogs.msdn.com/b/vsarabic/archive/2011/08/21/text-rendering.aspx // Devanagari example: // http://www.omniglot.com/language/articles/devanagari.htm // UTF16 non-zero plane exahere: // https://en.wikipedia.org/wiki/UTF-16#Examples // http://www.i18nguy.com/unicode-example-plane1.html // "Displaying Text with Uniscribe" // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317792%28v=vs.85%29.aspx internal static TextItems AnalyzeText( TextServiceUniscribe service, IntPtr hdc, IntPtr hText, FontRunInfo[] fontRuns) { TextItems o = new TextItems(service); try { o.fontRuns = fontRuns; for (int i = 0; i < fontRuns.Length; i++) { o.count += fontRuns[i].count; o.lineHeight = Math.Max(o.lineHeight, fontRuns[i].fontHeight); } if (o.count == 0) { return o; } int hr; // Lay Out Text Using Uniscribe // 1. Call ScriptRecordDigitSubstitution only when starting or when receiving a WM_SETTINGCHANGE message. // 2. (Optional) Call ScriptIsComplex to determine if the paragraph requires complex processing. hr = ScriptIsComplex( hText, o.count, ScriptIsComplexFlags.SIC_ASCIIDIGIT | ScriptIsComplexFlags.SIC_COMPLEX); if (hr < 0) { Marshal.ThrowExceptionForHR(hr); } // Optional: if S_FALSE (1) is returned, one can fall back to TextRenderer and simplified hit testing // 3. (Optional) If using Uniscribe to handle bidirectional text and/or digit substitution, call // ScriptApplyDigitSubstitution to prepare the SCRIPT_CONTROL and SCRIPT_STATE structures as inputs // to ScriptItemize. If skipping this step, but still requiring digit substitution, substitute // national digits for Unicode U+0030 through U+0039 (European digits). For information about digit // substitution, see Digit Shapes. hr = ScriptApplyDigitSubstitution( IntPtr.Zero, out o.sControl, out o.sState); if (hr < 0) { Marshal.ThrowExceptionForHR(hr); } // 4. Call ScriptItemize to divide the paragraph into items. If not using Uniscribe for digit // substitution and the bidirectional order is known, for example, because of the keyboard layout // used to enter the character, call ScriptItemize. In the call, provide null pointers for the // SCRIPT_CONTROL and SCRIPT_STATE structures. This technique generates items by use of the shaping // engine only, and the items can be reordered using the engine information. // Note: Typically, applications that work only with left-to-right scripts and without any digit // substitution should pass null pointers for the SCRIPT_CONTROL and SCRIPT_STATE structures. { int cMaxItems = 8; SCRIPT_ITEM[] sItems; OPENTYPE_TAG[] sTags; while (true) { sItems = new SCRIPT_ITEM[cMaxItems + 1]; // method adds terminator; sTags = new OPENTYPE_TAG[cMaxItems]; hr = ScriptItemizeOpenType( hText, o.count, cMaxItems, ref o.sControl, ref o.sState, sItems, sTags, out o.cItems); if (hr == E_OUTOFMEMORY) { cMaxItems *= 2; continue; } if (hr < 0) { Marshal.ThrowExceptionForHR(hr); } break; } o.sItems = sItems; o.sTags = sTags; } // 5. Merge the item information with the run information to produce ranges. { int fontRunOffset = 0; int f = 0; int i = 0; while ((f < o.fontRuns.Length) && (i < o.cItems)) { if (fontRunOffset + o.fontRuns[f].count > o.sItems[i + 1].iCharPos) { // run is longer - current item remains intact; advance to next i++; continue; } else if (fontRunOffset + o.fontRuns[f].count < o.sItems[i + 1].iCharPos) { // item too long - split Array.Resize(ref o.sItems, o.cItems + 1 + 1); Array.Copy(o.sItems, i, o.sItems, i + 1, o.sItems.Length - (i + 1)); Array.Resize(ref o.sTags, o.cItems + 1); Array.Copy(o.sTags, i, o.sTags, i + 1, o.sTags.Length - (i + 1)); o.cItems++; o.sItems[i + 1].iCharPos = fontRunOffset + o.fontRuns[f].count; } Debug.Assert(fontRunOffset + o.fontRuns[f].count == o.sItems[i + 1].iCharPos); fontRunOffset += o.fontRuns[f].count; f++; i++; } Debug.Assert(fontRunOffset == o.count); Debug.Assert(o.sItems[o.cItems].iCharPos == o.count); } // 6. Call ScriptShape to identify clusters and generate glyphs. o.sItemsExtra = new ItemInfo[o.cItems]; o.logAttrs = new SCRIPT_LOGATTR[o.count]; for (int i = 0; i < o.cItems; i++) { int start = o.sItems[i].iCharPos; int length = o.sItems[i + 1].iCharPos - start; Font font = null; for (int f = 0, pos = 0; f < o.fontRuns.Length; pos += o.fontRuns[f].count, f++) { font = o.fontRuns[f].font; if (pos < start + length) { break; } } int cMaxGlyphs = (3 * o.count / 2) + 16; // recommended starting value short[] glyphs; short[] logicalClusters; SCRIPT_VISATTR[] visAttrs; // superceded by glyphProps SCRIPT_GLYPHPROP[] glyphProps; int cGlyphs; int fallbackLevel = 0; Font fallbackFont = null; SCRIPT_CHARPROP[] charProps = new SCRIPT_CHARPROP[length]; while (true) { glyphs = new short[cMaxGlyphs]; logicalClusters = new short[cMaxGlyphs]; visAttrs = new SCRIPT_VISATTR[cMaxGlyphs]; glyphProps = new SCRIPT_GLYPHPROP[cMaxGlyphs]; bool needFallback = false; int fontCacheIndex = service.FontCacheIndex(font); GDIFont gdiFont; if (!service.fontToHFont.TryGetValue(font, out gdiFont)) { gdiFont = new GDIFont(font); service.fontToHFont.Add(font, gdiFont); } GDI.SelectObject(hdc, gdiFont); #if false // choose old or OpenType API hr = ScriptShape( hdc, ref service.caches[fontCacheIndex].cache, new IntPtr(hText.ToInt64() + start * 2), length, cMaxGlyphs, ref o.sItems[i].a, glyphs, logicalClusters, visAttrs, out cGlyphs); #else hr = ScriptShapeOpenType( hdc, ref service.caches[fontCacheIndex].cache, ref o.sItems[i].a, o.sTags[i], // tagScript o.sTags[i], // tagLangSys -- right thing to pass here? null, // rcRangeChars null, // rpRangeProperties 0, // cRanges new IntPtr(hText.ToInt64() + start * 2), length, cMaxGlyphs, logicalClusters, charProps, glyphs, glyphProps, // supercedes visAttrs out cGlyphs); #endif if (hr == E_OUTOFMEMORY) { cMaxGlyphs *= 2; glyphs = new short[cMaxGlyphs]; logicalClusters = new short[cMaxGlyphs]; visAttrs = new SCRIPT_VISATTR[cMaxGlyphs]; continue; } if (hr == USP_E_SCRIPT_NOT_IN_FONT) { needFallback = true; goto FontFallback; } if (hr < 0) { Marshal.ThrowExceptionForHR(hr); } // 7. If ScriptShape returns the code USP_E_SCRIPT_NOT_IN_FONT or S_OK with the output containing // missing glyphs, select characters from a different font. Either substitute another font or disable // shaping by setting the eScript member of the SCRIPT_ANALYSIS structure passed to ScriptShape to // SCRIPT_UNDEFINED. For more information, see Using Font Fallback. SCRIPT_FONTPROPERTIES sfp; ScriptGetFontProperties( hdc, ref service.caches[fontCacheIndex].cache, out sfp); for (int j = 0; j < cGlyphs; j++) { if (glyphs[j] == sfp.wgDefault) { needFallback = true; break; } } FontFallback: if (needFallback) { // What worked: // https://code.google.com/p/chromium/codesearch#chromium/src/ui/gfx/font_fallback_win.cc&q=uniscribe&sq=package:chromium&l=283&dr=CSs // http://stackoverflow.com/questions/16828868/how-to-automatically-choose-most-suitable-font-for-different-language // What didn't work: // The totally unhelpful Uniscribe font/script fallback documentation: // https://msdn.microsoft.com/en-us/library/windows/desktop/dd374105%28v=vs.85%29.aspx // MSDN Globalization how-to page about font fallback: // https://msdn.microsoft.com/en-us/goglobal/bb688134.aspx // ScriptShape page: // https://msdn.microsoft.com/en-us/library/windows/desktop/dd368564(v=vs.85).aspx fallbackLevel++; switch (fallbackLevel) { case 1: GDI.LOGFONT fallbackLF; if (GetUniscribeFallbackFont( font, new IntPtr(hText.ToInt64() + 2 * start), length, out fallbackLF)) { fallbackFont = null; foreach (KeyValuePair<GDI.LOGFONT, Font> item in service.fallbackFonts) { if (String.Equals(item.Key.lfFaceName, fallbackLF.lfFaceName) && (item.Key.lfCharSet == fallbackLF.lfCharSet) && (item.Key.lfHeight == fallbackLF.lfHeight)) { fallbackFont = item.Value; break; } } if (fallbackFont == null) { fallbackFont = Font.FromLogFont(fallbackLF); service.fallbackFonts.Add(new KeyValuePair<GDI.LOGFONT, Font>(fallbackLF, fallbackFont)); } font = fallbackFont; continue; } continue; case 2: if (o.sItems[i].a.eScript != SCRIPT_UNDEFINED) { o.sItems[i].a.eScript = SCRIPT_UNDEFINED; } continue; default: // give up break; } } break; } // 8. Call ScriptPlace to generate advance widths and x and y positions for the glyphs in each // successive range. This is the first step for which text size becomes a consideration. int[] iAdvances = new int[cGlyphs]; GOFFSET[] goffsets = new GOFFSET[cGlyphs]; ABC abc = new ABC(); { int fontCacheIndex = service.FontCacheIndex(font); #if false // choose old or OpenType API hr = ScriptPlace( hdc, ref service.caches[fontCacheIndex].cache, glyphs, cGlyphs, visAttrs, ref o.sItems[i].a, iAdvances, goffsets, out abc); #else hr = ScriptPlaceOpenType( hdc, ref service.caches[fontCacheIndex].cache, ref o.sItems[i].a, o.sTags[i], // tagScript o.sTags[i], // tagLangSys -- right thing to pass here? null, // rcRangeChars null, // rpRangeProperties 0, // cRanges new IntPtr(hText.ToInt64() + start * 2), logicalClusters, charProps, length, glyphs, glyphProps, cGlyphs, iAdvances, goffsets, out abc); #endif if (hr < 0) { Marshal.ThrowExceptionForHR(hr); } } o.sItemsExtra[i].glyphs = glyphs; o.sItemsExtra[i].logicalClusters = logicalClusters; o.sItemsExtra[i].visAttrs = visAttrs; o.sItemsExtra[i].cGlyphs = cGlyphs; o.sItemsExtra[i].charProps = charProps; o.sItemsExtra[i].glyphProps = glyphProps; o.sItemsExtra[i].iAdvances = iAdvances; o.sItemsExtra[i].goffsets = goffsets; o.sItemsExtra[i].abc = abc; o.sItemsExtra[i].fallbackFont = fallbackFont; // 9. Sum the range sizes until the line overflows. // 10. Break the range on a word boundary by using the fSoftBreak and fWhiteSpace members in the // logical attributes. To break a single character cluster off the run, use the information returned // by calling ScriptBreak. // Note: Decide if the first code point of a range should be a word break point because the last // character of the previous range requires it. For example, if one range ends in a comma, consider // the first character of the next range to be a word break point. SCRIPT_LOGATTR[] logAttrs1 = new SCRIPT_LOGATTR[o.count]; hr = ScriptBreak( new IntPtr(hText.ToInt64() + 2 * start), length, ref o.sItems[i].a, logAttrs1); if (hr < 0) { Marshal.ThrowExceptionForHR(hr); } Array.Copy(logAttrs1, 0, o.logAttrs, o.sItems[i].iCharPos, length); // 11. Repeat steps 6 through 10 for each line in the paragraph. However, if breaking the last run // on the line, call ScriptShape to reshape the remaining part of the run as the first run on the // next line. } // Display Text Using Uniscribe // 1. For each run, do the following: // a. If the style has changed since the last run, update the handle to the device context by releasing // and getting it again. // b. Call ScriptShape to generate glyphs for the run. // c. Call ScriptPlace to generate an advance width and an x,y offset for each glyph. // 2. Do the following to establish the correct visual order for the runs in the line: // a. Extract an array of bidirectional embedding levels, one per range. The embedding level is // given by (SCRIPT_ITEM) si.(SCRIPT_ANALYSIS) a. (SCRIPT_STATE) s.uBidiLevel. // b. Pass this array to ScriptLayout to generate a map of visual positions to logical positions. byte[] bidiEmbeddingLevels = new byte[o.cItems]; for (int i = 0; i < o.cItems; i++) { bidiEmbeddingLevels[i] = (byte)o.sItems[i].a.s.uBidiLevel; } o.iVisualToLogical = new int[o.cItems]; o.iLogicalToVisual = new int[o.cItems]; hr = ScriptLayout( o.cItems, bidiEmbeddingLevels, o.iVisualToLogical, o.iLogicalToVisual); if (hr < 0) { Marshal.ThrowExceptionForHR(hr); } for (int i = 0; i < o.cItems; i++) { for (int j = 0; j < o.sItemsExtra[i].iAdvances.Length; j++) { o.sItemsExtra[i].totalWidth += o.sItemsExtra[i].iAdvances[j]; } } } catch (Exception) { o.Dispose(); throw; } return o; }