From 061ac58389f513a51c69f5b6f22885e2485e7832 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Tue, 22 Mar 2022 14:51:11 -0700 Subject: [PATCH] Add APIs to load fonts from file - Added FontFamily.FromFiles/FromStreams - Added Font.FromFile/FromStream - Added FontTypeface(string/Stream) - Add ability to use typographic font names in WinForms by setting `FontsHandler.UseTypographicFonts = true` (off by default for now) Fixes #2120 --- src/Eto.Direct2D/Drawing/FontFamilyHandler.cs | 15 +- .../Drawing/FontTypefaceHandler.cs | 18 + src/Eto.Gtk/Drawing/FontFamilyHandler.cs | 48 ++- src/Eto.Gtk/Drawing/FontTypefaceHandler.cs | 119 ++++++ src/Eto.Gtk/Drawing/FontsHandler.cs | 9 + src/Eto.Gtk/NativeMacMethods.cs | 146 ++++++++ src/Eto.Gtk/NativeMethods.cs | 148 +++++++- src/Eto.Gtk/NativeMethods.tt | 79 +++- src/Eto.Gtk/Platform.cs | 1 + src/Eto.Mac/Drawing/EtoFontManager.cs | 14 +- src/Eto.Mac/Drawing/FontFamilyHandler.cs | 76 +++- src/Eto.Mac/Drawing/FontHandler.cs | 51 +-- src/Eto.Mac/Drawing/FontTypefaceHandler.cs | 52 ++- src/Eto.Mac/Platform.cs | 1 + src/Eto.WinForms/Drawing/FontFamilyHandler.cs | 217 ++++++++++- src/Eto.WinForms/Drawing/FontHandler.cs | 68 +--- .../Drawing/FontTypefaceHandler.cs | 109 +++++- src/Eto.WinForms/Drawing/FontsHandler.cs | 114 +++++- src/Eto.WinForms/Eto.WinForms.csproj | 3 + .../Forms/Controls/RichTextAreaHandler.cs | 12 +- .../Forms/Controls/TextAreaHandler.cs | 2 + src/Eto.WinForms/Forms/WindowsControl.cs | 47 +-- src/Eto.WinForms/Platform.cs | 1 + src/Eto.WinForms/WinConversions.cs | 7 +- src/Eto.Wpf/Drawing/FontFamilyHandler.cs | 175 +++++++-- src/Eto.Wpf/Drawing/FontTypefaceHandler.cs | 112 +++++- src/Eto.Wpf/Eto.Wpf.csproj | 3 + .../Forms/Controls/RichTextAreaHandler.cs | 2 + src/Eto.Wpf/Platform.cs | 1 + src/Eto/Drawing/Font.cs | 39 +- src/Eto/Drawing/FontFamily.cs | 81 +++++ src/Eto/Drawing/FontTypeface.cs | 74 +++- src/Shared/OpenTypeFontInfo.cs | 339 ++++++++++++++++++ .../Sections/Controls/RichTextAreaSection.cs | 17 +- .../Sections/Dialogs/FontDialogSection.cs | 145 +++++++- 35 files changed, 2132 insertions(+), 213 deletions(-) create mode 100644 src/Eto.Gtk/NativeMacMethods.cs create mode 100755 src/Shared/OpenTypeFontInfo.cs diff --git a/src/Eto.Direct2D/Drawing/FontFamilyHandler.cs b/src/Eto.Direct2D/Drawing/FontFamilyHandler.cs index 3a4a215828..f0877f266d 100644 --- a/src/Eto.Direct2D/Drawing/FontFamilyHandler.cs +++ b/src/Eto.Direct2D/Drawing/FontFamilyHandler.cs @@ -7,6 +7,7 @@ using sd = SharpDX.Direct2D1; using sw = SharpDX.DirectWrite; using System.Globalization; +using System.IO; namespace Eto.Direct2D.Drawing { @@ -30,11 +31,11 @@ public string LocalizedName } } - FontTypeface[] typefaces; + FontTypeface[] _typefaces; public IEnumerable Typefaces { get { - return typefaces ?? (typefaces = Enumerable.Range(0, Control.FontCount) + return _typefaces ?? (_typefaces = Enumerable.Range(0, Control.FontCount) .Select(r => Control.GetFont(r)) .Select(r => new FontTypeface(Widget, new FontTypefaceHandler(r))) .ToArray()); @@ -79,5 +80,15 @@ public void Create(string familyName) Control = FontHandler.GetFontFamily(translatedName); } + + public void CreateFromFiles(IEnumerable fileNames) + { + throw new NotImplementedException(); + } + + public void CreateFromStreams(IEnumerable streams) + { + throw new NotImplementedException(); + } } } diff --git a/src/Eto.Direct2D/Drawing/FontTypefaceHandler.cs b/src/Eto.Direct2D/Drawing/FontTypefaceHandler.cs index ec6f52c0eb..13df8141dc 100755 --- a/src/Eto.Direct2D/Drawing/FontTypefaceHandler.cs +++ b/src/Eto.Direct2D/Drawing/FontTypefaceHandler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using Eto; @@ -39,6 +40,8 @@ public string LocalizedName public bool IsSymbol => Font.IsSymbolFont; + public FontFamily Family { get; private set; } + public bool HasCharacterRanges(IEnumerable> ranges) { foreach (var range in ranges) @@ -49,5 +52,20 @@ public bool HasCharacterRanges(IEnumerable> ranges) } return true; } + + public void Create(Stream stream) + { + throw new NotSupportedException(); + } + + public void Create(string fileName) + { + throw new NotSupportedException(); + } + + public void Create(FontFamily family) + { + Family = family; + } } } diff --git a/src/Eto.Gtk/Drawing/FontFamilyHandler.cs b/src/Eto.Gtk/Drawing/FontFamilyHandler.cs index c019743657..6314692b9a 100755 --- a/src/Eto.Gtk/Drawing/FontFamilyHandler.cs +++ b/src/Eto.Gtk/Drawing/FontFamilyHandler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using Eto.Drawing; @@ -12,9 +13,12 @@ public class FontFamilyHandler : WidgetHandler, Fo public string LocalizedName => Control?.Name ?? Name; - public IEnumerable Typefaces + FontTypeface[] _typefaces; + public IEnumerable Typefaces => _typefaces ?? (_typefaces = GetTypefaces().ToArray()); + + IEnumerable GetTypefaces() { - get { return Control.Faces.Where(r => r != null).Select(r => new FontTypeface(Widget, new FontTypefaceHandler(r))); } + return Control.Faces.Where(r => r != null).Select(r => new FontTypeface(Widget, new FontTypefaceHandler(r))); } public FontFamilyHandler() @@ -26,6 +30,14 @@ public FontFamilyHandler(Pango.FontFamily pangoFamily) Control = pangoFamily; Name = Control.Name; } + + public FontFamilyHandler(string familyName, FontTypefaceHandler typeface) + { + Name = familyName; + _typefaces = new[] { typeface.Widget }; + var fm = FontsHandler.Context.FontMap; + Control = FindCorrectedFamily(familyName); + } public static Pango.FontFamily FindCorrectedFamily(string familyName) { @@ -106,6 +118,38 @@ public static Pango.FontFamily GetFontFamily(string familyName) return null; return FontsHandler.Context.Families.FirstOrDefault(r => string.Equals(r.Name, familyName, StringComparison.InvariantCultureIgnoreCase)); } + + public void CreateFromFiles(IEnumerable fileNames) + { + foreach (var fileName in fileNames) + { + var familyName = FontTypefaceHandler.LoadFontFromFile(fileName); + if (Name == null) + Name = familyName; + else if (Name != familyName) + throw new InvalidOperationException($"Family name of the supplied font files do not match. '{Name}' and '{familyName}'"); + + } + + FontsHandler.ResetFontMap(); + Control = FindCorrectedFamily(Name); + } + + public void CreateFromStreams(IEnumerable streams) + { + foreach (var stream in streams) + { + var familyName = FontTypefaceHandler.LoadFontFromStream(stream); + if (Name == null) + Name = familyName; + else if (Name != familyName) + throw new InvalidOperationException($"Family name of the supplied font files do not match. '{Name}' and '{familyName}'"); + + } + + FontsHandler.ResetFontMap(); + Control = FindCorrectedFamily(Name); + } } } diff --git a/src/Eto.Gtk/Drawing/FontTypefaceHandler.cs b/src/Eto.Gtk/Drawing/FontTypefaceHandler.cs index 1cb2586e6a..186a9c3069 100644 --- a/src/Eto.Gtk/Drawing/FontTypefaceHandler.cs +++ b/src/Eto.Gtk/Drawing/FontTypefaceHandler.cs @@ -2,7 +2,9 @@ using Eto.Forms; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; namespace Eto.GtkSharp.Drawing { @@ -13,6 +15,10 @@ public FontTypefaceHandler(Pango.FontFace pangoFace) Control = pangoFace; } + public FontTypefaceHandler() + { + } + public string Name => Control?.FaceName; public string LocalizedName => Name; @@ -36,6 +42,8 @@ public FontStyle FontStyle public bool IsSymbol => false; // todo, how do we get font info? + public FontFamily Family { get; private set; } + static Pango.AttrList noFallbackAttributes; static object noFallbackLock = new object(); @@ -69,6 +77,117 @@ public bool HasCharacterRanges(IEnumerable> ranges) } return true; } + + public void Create(Stream stream) + { + var familyName = LoadFontFromStream(stream); + FontsHandler.ResetFontMap(); + CreateFromFamilyName(familyName); + } + + public unsafe void Create(string fileName) + { + var familyName = LoadFontFromFile(fileName); + FontsHandler.ResetFontMap(); + CreateFromFamilyName(familyName); + } + + private void CreateFromFamilyName(string familyName) + { + var familyHandler = new FontFamilyHandler(familyName, this); + Family = new FontFamily(familyHandler); + if (familyHandler.Control == null) + throw new ArgumentException("Font could not be loaded"); + Control = familyHandler.Control.Faces[0]; + } + + internal static unsafe string LoadFontFromFile(string fileName) + { +#if GTKCORE + // note: FontMap is null on Mac currently. It's likely a bug. + if (FontsHandler.Context.FontMap?.NativeType.ToString() == "PangoCairoFcFontMap") + { + var fcconfig = NativeMethods.FcConfigGetCurrent(); + + if (!NativeMethods.FcConfigAppFontAddFile(fcconfig, fileName)) + throw new ArgumentException(nameof(fileName), "Could not add font to fontconfig"); + + var fcfontsPtr = NativeMethods.FcConfigGetFonts(fcconfig, NativeMethods.FcSetName.FcSetApplication); + var fcfonts = Marshal.PtrToStructure(fcfontsPtr); + IntPtr[] fonts = new IntPtr[fcfonts.nfont]; + Marshal.Copy(fcfonts.fonts, fonts, 0, fcfonts.nfont); + + // we're assuming, but probably correct that the last font added goes to the last entry in the array. + var fontDescriptionPtr = NativeMethods.pango_fc_font_description_from_pattern(fonts[fonts.Length - 1], false); + var fontdesc = new Pango.FontDescription(fontDescriptionPtr); + return fontdesc.Family; + + } + else if (EtoEnvironment.Platform.IsMac) + { + IntPtr provider = NativeMacMethods.CGDataProviderCreateWithFilename(fileName); + var cgfont = NativeMacMethods.CGFontCreateWithDataProvider(provider); + NativeMacMethods.CGDataProviderRelease(provider); + var ctfont = NativeMacMethods.CTFontCreateWithGraphicsFont(cgfont, 10, IntPtr.Zero, IntPtr.Zero); + var fontFamily = NativeMacMethods.CTFontCopyName(ctfont, NativeMacMethods.CTFontNameKeyFamily); + NativeMacMethods.CFRelease(ctfont); + + NativeMacMethods.CTFontManagerRegisterGraphicsFont(cgfont, out var error); + + return NativeMacMethods.CFStringToString(fontFamily); + } + + // todo: What do we do on Windows?? Maybe if someone cares enough they can help here.. +#endif + throw new NotSupportedException("This platform does not support loading fonts directly"); + } + + internal static unsafe string LoadFontFromStream(Stream stream) + { +#if GTKCORE + // note: FontMap is null on Mac currently. It's likely a bug. + if (FontsHandler.Context.FontMap?.NativeType.ToString() == "PangoCairoFcFontMap") + { + // need to save to a temp file and use that. + // https://gitlab.freedesktop.org/fontconfig/fontconfig/-/issues/12 + var tempFileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + using (var fs = File.Create(tempFileName)) + { + stream.CopyTo(fs); + } + return LoadFontFromFile(tempFileName); + } + else if (EtoEnvironment.Platform.IsMac) + { + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + var buffer = ms.ToArray(); + IntPtr provider; + fixed (byte* p = &buffer[0]) + { + provider = NativeMacMethods.CGDataProviderCreateWithData(IntPtr.Zero, (IntPtr)p, new IntPtr(buffer.Length), IntPtr.Zero); + } + var cgfont = NativeMacMethods.CGFontCreateWithDataProvider(provider); + NativeMacMethods.CGDataProviderRelease(provider); + var ctfont = NativeMacMethods.CTFontCreateWithGraphicsFont(cgfont, 10, IntPtr.Zero, IntPtr.Zero); + var fontFamily = NativeMacMethods.CTFontCopyName(ctfont, NativeMacMethods.CTFontNameKeyFamily); + NativeMacMethods.CFRelease(ctfont); + + NativeMacMethods.CTFontManagerRegisterGraphicsFont(cgfont, out var error); + + return NativeMacMethods.CFStringToString(fontFamily); + } + } + // todo: What do we do on Windows?? Maybe if someone cares enough they can help here.. +#endif + throw new NotSupportedException("This platform does not support loading fonts directly"); + } + + public void Create(FontFamily family) + { + Family = family; + } } } diff --git a/src/Eto.Gtk/Drawing/FontsHandler.cs b/src/Eto.Gtk/Drawing/FontsHandler.cs index f9c0d01181..72a50302da 100644 --- a/src/Eto.Gtk/Drawing/FontsHandler.cs +++ b/src/Eto.Gtk/Drawing/FontsHandler.cs @@ -21,6 +21,15 @@ public static Pango.Context Context return context; } } + + public static void ResetFontMap() + { + IntPtr fontMapPtr = NativeMethods.pango_cairo_font_map_new(); + NativeMethods.pango_cairo_font_map_set_default(fontMapPtr); + ResetContext(); + } + + public static void ResetContext() => context = null; public IEnumerable AvailableFontFamilies { diff --git a/src/Eto.Gtk/NativeMacMethods.cs b/src/Eto.Gtk/NativeMacMethods.cs new file mode 100644 index 0000000000..b550609717 --- /dev/null +++ b/src/Eto.Gtk/NativeMacMethods.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Eto.GtkSharp +{ + static class NativeMacMethods + { + public const string CoreGraphicsLibrary = "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/CoreGraphics.framework/CoreGraphics"; + public const string CoreTextLibrary = "/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreText.framework/CoreText"; + public const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + public const string SystemLibrary = "/usr/lib/libSystem.dylib"; + + public struct CFRange + { + IntPtr loc; // defined as 'long' in native code + IntPtr len; // defined as 'long' in native code + + public int Location + { + get { return loc.ToInt32(); } + } + + public int Length + { + get { return len.ToInt32(); } + } + + public long LongLocation + { + get { return loc.ToInt64(); } + } + + public long LongLength + { + get { return len.ToInt64(); } + } + + public CFRange(int loc, int len) + : this((long)loc, (long)len) + { + } + + public CFRange(long l, long len) + { + this.loc = new IntPtr(l); + this.len = new IntPtr(len); + } + + public override string ToString() + { + return string.Format("CFRange [Location: {0} Length: {1}]", loc, len); + } + } + + [DllImport(CoreGraphicsLibrary)] + public extern static IntPtr CGDataProviderCreateWithFilename(string filename); + + [DllImport(CoreGraphicsLibrary)] + public extern static IntPtr CGDataProviderCreateWithData(IntPtr info, IntPtr data, IntPtr size, IntPtr releaseData); + + [DllImport(CoreGraphicsLibrary)] + public extern static IntPtr CGFontCreateWithDataProvider(IntPtr provider); + + [DllImport(CoreTextLibrary)] + public static extern bool CTFontManagerRegisterGraphicsFont(IntPtr cgfont, out IntPtr error); + + [DllImport(CoreGraphicsLibrary)] + public extern static IntPtr CGFontCopyFullName(IntPtr font); + + [DllImport(CoreFoundationLibrary, CharSet = CharSet.Unicode)] + public extern static int CFStringGetLength(IntPtr handle); + + [DllImport(CoreFoundationLibrary, CharSet = CharSet.Unicode)] + public extern static IntPtr CFStringGetCharactersPtr(IntPtr handle); + + [DllImport(CoreFoundationLibrary, CharSet = CharSet.Unicode)] + public extern static IntPtr CFStringGetCharacters(IntPtr handle, CFRange range, IntPtr buffer); + + public static string CFStringToString(IntPtr handle) + { + if (handle == IntPtr.Zero) + return null; + + string str; + + int l = CFStringGetLength(handle); + IntPtr u = CFStringGetCharactersPtr(handle); + IntPtr buffer = IntPtr.Zero; + if (u == IntPtr.Zero) + { + CFRange r = new CFRange(0, l); + buffer = Marshal.AllocCoTaskMem(l * 2); + CFStringGetCharacters(handle, r, buffer); + u = buffer; + } + unsafe + { + str = new string((char*)u, 0, l); + } + + if (buffer != IntPtr.Zero) + Marshal.FreeCoTaskMem(buffer); + + return str; + } + + [DllImport(SystemLibrary)] + public static extern IntPtr dlopen(string path, int mode); + + [DllImport(SystemLibrary)] + public static extern IntPtr dlsym(IntPtr handle, string symbol); + + static IntPtr? _CoreTextLibraryPtr; + static IntPtr CoreTextLibraryPtr => _CoreTextLibraryPtr ?? (_CoreTextLibraryPtr = dlopen(CoreTextLibrary, 0)) ?? IntPtr.Zero; + + static IntPtr? _CTFontNameKeyFamily; + public static IntPtr CTFontNameKeyFamily => _CTFontNameKeyFamily ?? (_CTFontNameKeyFamily = GetStringConstant(CoreTextLibraryPtr, "kCTFontFamilyNameKey")) ?? IntPtr.Zero; + + public static IntPtr GetStringConstant(IntPtr library, string name) + { + var indirect = dlsym(library, name); + if (indirect == IntPtr.Zero) + return IntPtr.Zero; + return Marshal.ReadIntPtr(indirect); + } + + + [DllImport(CoreTextLibrary)] + public static extern IntPtr CTFontCreateWithGraphicsFont(IntPtr cgfontRef, double size, IntPtr affine, IntPtr attrs); + + [DllImport(CoreTextLibrary)] + public static extern IntPtr CTFontCopyName(IntPtr font, IntPtr nameKey); + + [DllImport(CoreGraphicsLibrary)] + public extern static void CGDataProviderRelease(IntPtr handle); + + + [DllImport(CoreFoundationLibrary)] + internal extern static void CFRelease(IntPtr obj); + + + } +} \ No newline at end of file diff --git a/src/Eto.Gtk/NativeMethods.cs b/src/Eto.Gtk/NativeMethods.cs index 7a100ad73b..49cd61c0ff 100755 --- a/src/Eto.Gtk/NativeMethods.cs +++ b/src/Eto.Gtk/NativeMethods.cs @@ -15,6 +15,21 @@ static class NativeMethods const string ver = "3"; #endif + public enum FcSetName : int + { + FcSetSystem = 0, + FcSetApplication = 1 + } + + [StructLayout(LayoutKind.Sequential)] + public struct FcFontSet + { + public int nfont; + public int sfont; + public IntPtr fonts; + } + + static class NMWindows { #if GTK2 @@ -23,10 +38,14 @@ static class NMWindows const string plat = ""; #endif const string ext = "-0.dll"; + const string extalt = ".dll"; const string libgobject = "libgobject-2.0" + ext; const string libgtk = "libgtk-" + plat + ver + ext; const string libgdk = "libgdk-" + plat + ver + ext; - const string libpango = "libpango-" + plat + ver + ext; + const string libpango = "libpango-1.0" + ext; + const string libpangocairo = "libpangocairo-1.0" + ext; + const string libpangoft2 = "libpangoft2-1.0" + ext; + const string libfontconfig = "libfontconfig" + extalt; const string libwebkit = "libwebkit2gtk-4.0.so.37"; [DllImport(libgobject, CallingConvention = CallingConvention.Cdecl)] @@ -187,9 +206,22 @@ static class NMWindows [DllImport(libgdk, CallingConvention = CallingConvention.Cdecl)] public extern static IntPtr gdk_pixbuf_get_from_window(IntPtr window, int x, int y, int width, int height); - [DllImport(libpango, CallingConvention = CallingConvention.Cdecl)] public extern static bool pango_font_has_char(IntPtr font, int wc); + [DllImport(libpangocairo, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr pango_cairo_font_map_new(); + [DllImport(libpangocairo, CallingConvention = CallingConvention.Cdecl)] + public extern static void pango_cairo_font_map_set_default(IntPtr fontMap); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr pango_fc_font_description_from_pattern(IntPtr fcpattern, bool includeSize); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static bool FcInit(); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr FcConfigGetCurrent(); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static bool FcConfigAppFontAddFile(IntPtr fc, string fileName); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr FcConfigGetFonts(IntPtr fc, FcSetName setName); } static class NMLinux @@ -200,10 +232,14 @@ static class NMLinux const string plat = ""; #endif const string ext = ".so.0"; + const string extalt = ".so.1"; const string libgobject = "libgobject-2.0" + ext; const string libgtk = "libgtk-" + plat + ver + ext; const string libgdk = "libgdk-" + plat + ver + ext; - const string libpango = "libpango-" + plat + ver + ext; + const string libpango = "libpango-1.0" + ext; + const string libpangocairo = "libpangocairo-1.0" + ext; + const string libpangoft2 = "libpangoft2-1.0" + ext; + const string libfontconfig = "libfontconfig" + extalt; const string libwebkit = "libwebkit2gtk-4.0.so.37"; [DllImport(libgobject, CallingConvention = CallingConvention.Cdecl)] @@ -364,9 +400,22 @@ static class NMLinux [DllImport(libgdk, CallingConvention = CallingConvention.Cdecl)] public extern static IntPtr gdk_pixbuf_get_from_window(IntPtr window, int x, int y, int width, int height); - [DllImport(libpango, CallingConvention = CallingConvention.Cdecl)] public extern static bool pango_font_has_char(IntPtr font, int wc); + [DllImport(libpangocairo, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr pango_cairo_font_map_new(); + [DllImport(libpangocairo, CallingConvention = CallingConvention.Cdecl)] + public extern static void pango_cairo_font_map_set_default(IntPtr fontMap); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr pango_fc_font_description_from_pattern(IntPtr fcpattern, bool includeSize); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static bool FcInit(); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr FcConfigGetCurrent(); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static bool FcConfigAppFontAddFile(IntPtr fc, string fileName); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr FcConfigGetFonts(IntPtr fc, FcSetName setName); } static class NMMac @@ -377,10 +426,14 @@ static class NMMac const string plat = ""; #endif const string ext = ".dylib"; + const string extalt = ".dylib"; const string libgobject = "libgobject-2.0" + ext; const string libgtk = "libgtk-" + plat + ver + ext; const string libgdk = "libgdk-" + plat + ver + ext; - const string libpango = "libpango-" + plat + ver + ext; + const string libpango = "libpango-1.0" + ext; + const string libpangocairo = "libpangocairo-1.0" + ext; + const string libpangoft2 = "libpangoft2-1.0" + ext; + const string libfontconfig = "libfontconfig" + extalt; const string libwebkit = "libwebkit2gtk-4.0.so.37"; [DllImport(libgobject, CallingConvention = CallingConvention.Cdecl)] @@ -541,9 +594,22 @@ static class NMMac [DllImport(libgdk, CallingConvention = CallingConvention.Cdecl)] public extern static IntPtr gdk_pixbuf_get_from_window(IntPtr window, int x, int y, int width, int height); - [DllImport(libpango, CallingConvention = CallingConvention.Cdecl)] public extern static bool pango_font_has_char(IntPtr font, int wc); + [DllImport(libpangocairo, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr pango_cairo_font_map_new(); + [DllImport(libpangocairo, CallingConvention = CallingConvention.Cdecl)] + public extern static void pango_cairo_font_map_set_default(IntPtr fontMap); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr pango_fc_font_description_from_pattern(IntPtr fcpattern, bool includeSize); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static bool FcInit(); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr FcConfigGetCurrent(); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static bool FcConfigAppFontAddFile(IntPtr fc, string fileName); + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static IntPtr FcConfigGetFonts(IntPtr fc, FcSetName setName); } public static string GetString(IntPtr handle) @@ -1099,5 +1165,75 @@ public static bool pango_font_has_char(IntPtr font, int wc) else return NMWindows.pango_font_has_char(font, wc); } + + public static IntPtr pango_cairo_font_map_new() + { + if (EtoEnvironment.Platform.IsLinux) + return NMLinux.pango_cairo_font_map_new(); + else if (EtoEnvironment.Platform.IsMac) + return NMMac.pango_cairo_font_map_new(); + else + return NMWindows.pango_cairo_font_map_new(); + } + + public static void pango_cairo_font_map_set_default(IntPtr fontMap) + { + if (EtoEnvironment.Platform.IsLinux) + NMLinux.pango_cairo_font_map_set_default(fontMap); + else if (EtoEnvironment.Platform.IsMac) + NMMac.pango_cairo_font_map_set_default(fontMap); + else + NMWindows.pango_cairo_font_map_set_default(fontMap); + } + + public static IntPtr pango_fc_font_description_from_pattern(IntPtr fcpattern, bool includeSize) + { + if (EtoEnvironment.Platform.IsLinux) + return NMLinux.pango_fc_font_description_from_pattern(fcpattern, includeSize); + else if (EtoEnvironment.Platform.IsMac) + return NMMac.pango_fc_font_description_from_pattern(fcpattern, includeSize); + else + return NMWindows.pango_fc_font_description_from_pattern(fcpattern, includeSize); + } + + public static bool FcInit() + { + if (EtoEnvironment.Platform.IsLinux) + return NMLinux.FcInit(); + else if (EtoEnvironment.Platform.IsMac) + return NMMac.FcInit(); + else + return NMWindows.FcInit(); + } + + public static IntPtr FcConfigGetCurrent() + { + if (EtoEnvironment.Platform.IsLinux) + return NMLinux.FcConfigGetCurrent(); + else if (EtoEnvironment.Platform.IsMac) + return NMMac.FcConfigGetCurrent(); + else + return NMWindows.FcConfigGetCurrent(); + } + + public static bool FcConfigAppFontAddFile(IntPtr fc, string fileName) + { + if (EtoEnvironment.Platform.IsLinux) + return NMLinux.FcConfigAppFontAddFile(fc, fileName); + else if (EtoEnvironment.Platform.IsMac) + return NMMac.FcConfigAppFontAddFile(fc, fileName); + else + return NMWindows.FcConfigAppFontAddFile(fc, fileName); + } + + public static IntPtr FcConfigGetFonts(IntPtr fc, FcSetName setName) + { + if (EtoEnvironment.Platform.IsLinux) + return NMLinux.FcConfigGetFonts(fc, setName); + else if (EtoEnvironment.Platform.IsMac) + return NMMac.FcConfigGetFonts(fc, setName); + else + return NMWindows.FcConfigGetFonts(fc, setName); + } } } diff --git a/src/Eto.Gtk/NativeMethods.tt b/src/Eto.Gtk/NativeMethods.tt index 4ba3340841..a11c6e0bf4 100755 --- a/src/Eto.Gtk/NativeMethods.tt +++ b/src/Eto.Gtk/NativeMethods.tt @@ -9,6 +9,7 @@ var pclass = new[] { "NMWindows", "NMLinux", "NMMac" }; var plat = new[] { "win32-", "x11-", "quartz-" }; var ext = new[] { "-0.dll", ".so.0", ".dylib" }; +var extalt = new[] { ".dll", ".so.1", ".dylib" }; var giomethods = new[] { @@ -84,7 +85,34 @@ var pangomethods = new[] "bool pango_font_has_char(IntPtr font, int wc)" }; -var methods = giomethods.Concat(gtkmethods).Concat(webkitmethods).Concat(gdkmethods).Concat(pangomethods).ToArray(); +var pangocairomethods = new [] +{ + "IntPtr pango_cairo_font_map_new()", + "void pango_cairo_font_map_set_default(IntPtr fontMap)" +}; + +var pangoft2methods = new [] +{ + "IntPtr pango_fc_font_description_from_pattern(IntPtr fcpattern, bool includeSize)" +}; + +var fontconfigmethods = new [] +{ + "bool FcInit()", + "IntPtr FcConfigGetCurrent()", + "bool FcConfigAppFontAddFile(IntPtr fc, string fileName)", + "IntPtr FcConfigGetFonts(IntPtr fc, FcSetName setName)" +}; + +var methods = giomethods + .Concat(gtkmethods) + .Concat(webkitmethods) + .Concat(gdkmethods) + .Concat(pangomethods) + .Concat(pangocairomethods) + .Concat(pangoft2methods) + .Concat(fontconfigmethods) + .ToArray(); #> using System; @@ -102,6 +130,21 @@ namespace Eto.GtkSharp #elif GTK3 const string ver = "3"; #endif + + public enum FcSetName : int + { + FcSetSystem = 0, + FcSetApplication = 1 + } + + [StructLayout(LayoutKind.Sequential)] + public struct FcFontSet + { + public int nfont; + public int sfont; + public IntPtr fonts; + } + <# for (int i = 0; i < pclass.Length; i++) { @@ -115,10 +158,14 @@ namespace Eto.GtkSharp const string plat = ""; #endif const string ext = "<#= ext[i] #>"; + const string extalt = "<#= extalt[i] #>"; const string libgobject = "libgobject-2.0" + ext; const string libgtk = "libgtk-" + plat + ver + ext; const string libgdk = "libgdk-" + plat + ver + ext; - const string libpango = "libpango-" + plat + ver + ext; + const string libpango = "libpango-1.0" + ext; + const string libpangocairo = "libpangocairo-1.0" + ext; + const string libpangoft2 = "libpangoft2-1.0" + ext; + const string libfontconfig = "libfontconfig" + extalt; const string libwebkit = "libwebkit2gtk-4.0.so.37"; <# foreach (var method in giomethods) @@ -164,11 +211,37 @@ namespace Eto.GtkSharp foreach (var method in pangomethods) { #> - [DllImport(libpango, CallingConvention = CallingConvention.Cdecl)] public extern static <#= method.StartsWith("string") ? "IntPtr" + method.Substring(6) : method #>; <# } +#> +<# + foreach (var method in pangocairomethods) + { +#> + [DllImport(libpangocairo, CallingConvention = CallingConvention.Cdecl)] + public extern static <#= method.StartsWith("string") ? "IntPtr" + method.Substring(6) : method #>; +<# + } +#> +<# + foreach (var method in pangoft2methods) + { +#> + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static <#= method.StartsWith("string") ? "IntPtr" + method.Substring(6) : method #>; +<# + } +#> +<# + foreach (var method in fontconfigmethods) + { +#> + [DllImport(libpangoft2, CallingConvention = CallingConvention.Cdecl)] + public extern static <#= method.StartsWith("string") ? "IntPtr" + method.Substring(6) : method #>; +<# + } #> } <# diff --git a/src/Eto.Gtk/Platform.cs b/src/Eto.Gtk/Platform.cs index 9cf7774c46..7a7d4ccd51 100644 --- a/src/Eto.Gtk/Platform.cs +++ b/src/Eto.Gtk/Platform.cs @@ -102,6 +102,7 @@ public static void AddTo(Eto.Platform p) // Drawing p.Add(() => new BitmapHandler()); p.Add(() => new FontFamilyHandler()); + p.Add(() => new FontTypefaceHandler()); p.Add(() => new FontHandler()); p.Add(() => new FontsHandler()); p.Add(() => new GraphicsHandler()); diff --git a/src/Eto.Mac/Drawing/EtoFontManager.cs b/src/Eto.Mac/Drawing/EtoFontManager.cs index 9dee3f1620..cead1c27fe 100644 --- a/src/Eto.Mac/Drawing/EtoFontManager.cs +++ b/src/Eto.Mac/Drawing/EtoFontManager.cs @@ -60,13 +60,17 @@ public override NSFont ConvertFont(NSFont fontObj, NSFontTraitMask trait) if (newName != null) { - foreach (var descriptor in AvailableMembersOfFontFamily(fontObj.FamilyName)) + var availableMembersOfFontFamily = AvailableMembersOfFontFamily(fontObj.FamilyName); + if (availableMembersOfFontFamily != null) { - var fontName = (string)Messaging.GetNSObject(descriptor.ValueAt(1)); - if (string.Equals(fontName, newName, StringComparison.OrdinalIgnoreCase)) + foreach (var descriptor in availableMembersOfFontFamily) { - var postScriptName = (string)Messaging.GetNSObject(descriptor.ValueAt(0)); - return NSFont.FromFontName(postScriptName, fontObj.PointSize); + var fontName = (string)Messaging.GetNSObject(descriptor.ValueAt(1)); + if (string.Equals(fontName, newName, StringComparison.OrdinalIgnoreCase)) + { + var postScriptName = (string)Messaging.GetNSObject(descriptor.ValueAt(0)); + return NSFont.FromFontName(postScriptName, fontObj.PointSize); + } } } } diff --git a/src/Eto.Mac/Drawing/FontFamilyHandler.cs b/src/Eto.Mac/Drawing/FontFamilyHandler.cs index 7adcfa6cf2..0fdc85e6e4 100644 --- a/src/Eto.Mac/Drawing/FontFamilyHandler.cs +++ b/src/Eto.Mac/Drawing/FontFamilyHandler.cs @@ -2,6 +2,8 @@ using Eto.Drawing; using System.Collections.Generic; using System.Linq; +using System.IO; +using MonoMac.CoreText; #if XAMMAC2 using AppKit; using Foundation; @@ -33,6 +35,9 @@ public string LocalizedName { // faceName cannot be null. Use this when it is fixed in xammac/monomac: // return NSFontManager.SharedFontManager.LocalizedNameForFamily(MacName, null); + if (MacName == null) + return Name; + var facePtr = IntPtr.Zero; #if XAMMAC && NET6_0_OR_GREATER var familyPtr = CFString.CreateNative(MacName); @@ -49,15 +54,16 @@ public string LocalizedName public NSFontTraitMask TraitMask { get; set; } - public IEnumerable Typefaces + IList _typefaces; + + public IEnumerable Typefaces => _typefaces ?? (_typefaces = GetTypefaces().ToArray()); + + IEnumerable GetTypefaces() { - get - { - var descriptors = NSFontManager.SharedFontManager.AvailableMembersOfFontFamily(MacName); - if (descriptors == null) - return Enumerable.Empty(); - return descriptors.Select(r => new FontTypeface(Widget, new FontTypefaceHandler(r))); - } + var descriptors = NSFontManager.SharedFontManager.AvailableMembersOfFontFamily(MacName); + if (descriptors == null) + return Enumerable.Empty(); + return descriptors.Select(r => new FontTypeface(Widget, new FontTypefaceHandler(r))); } public FontFamilyHandler() @@ -65,6 +71,12 @@ public FontFamilyHandler() TraitMask = (NSFontTraitMask)int.MaxValue; } + public FontFamilyHandler(CGFont cgfont, FontTypefaceHandler typeface) + { + _typefaces = new[] { typeface.Widget }; + Name = cgfont.FullName; + } + public FontFamilyHandler(string familyName) { Create(familyName); @@ -108,6 +120,54 @@ public FontTypeface GetFace(NSFont font, NSFontTraitMask? traits) faceHandler = new FontTypefaceHandler(font, traits); return new FontTypeface(Widget, faceHandler); } + + public void CreateFromFiles(IEnumerable fileNames) + { + var typefaces = new List(); + foreach (var fileName in fileNames) + { + using (var dataProvider = new CGDataProvider(fileName)) + { + typefaces.Add(GetTypeface(dataProvider)); + } + } + _typefaces = typefaces.ToArray(); + } + + public void CreateFromStreams(IEnumerable streams) + { + var typefaces = new List(); + foreach (var stream in streams) + { + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + var bytes = ms.ToArray(); + using (var dataProvider = new CGDataProvider(bytes, 0, bytes.Length)) + { + typefaces.Add(GetTypeface(dataProvider)); + } + } + } + _typefaces = typefaces.ToArray(); + } + + private FontTypeface GetTypeface(CGDataProvider dataProvider) + { + var cgfont = CGFont.CreateFromProvider(dataProvider); + var ctfont = new CTFont(cgfont, 10, null); + var currentName = ctfont.GetName(CTFontNameKey.Family); + var faceName = ctfont.GetName(CTFontNameKey.SubFamily); + + if (Name == null) + Name = currentName; + else if (Name != currentName) + throw new InvalidOperationException($"Family name of the supplied font files do not match. '{Name}' and '{currentName}'"); + + var typefaceHandler = new FontTypefaceHandler(cgfont, faceName); + var typeface = new FontTypeface(Widget, typefaceHandler); + return typeface; + } } } diff --git a/src/Eto.Mac/Drawing/FontHandler.cs b/src/Eto.Mac/Drawing/FontHandler.cs index 323e31887e..2ffeb9078a 100644 --- a/src/Eto.Mac/Drawing/FontHandler.cs +++ b/src/Eto.Mac/Drawing/FontHandler.cs @@ -16,6 +16,7 @@ using MonoMac.ObjCRuntime; using MonoMac.CoreAnimation; using MonoMac.CoreImage; +using MonoMac.CoreText; #if Mac64 using nfloat = System.Double; using nint = System.Int64; @@ -47,10 +48,10 @@ namespace Eto.Mac.Drawing { public class FontHandler : WidgetHandler, Font.IHandler { - FontFamily family; - FontTypeface typeface; - FontStyle? style; - FontDecoration decoration; + FontFamily _family; + FontTypeface _typeface; + FontStyle? _style; + FontDecoration _decoration; NSDictionary _attributes; FormattedText _formattedText; @@ -71,21 +72,21 @@ public FontHandler(NSFont font) public FontHandler(NSFont font, FontDecoration decoration) { Control = font; - this.decoration = decoration; + _decoration = decoration; } public FontHandler(NSFont font, FontStyle style) { Control = font; - this.style = style; + _style = style; } public void Create(FontTypeface face, float size, FontDecoration decoration) { - typeface = face; - family = face.Family; + _typeface = face; + _family = face.Family; Control = ((FontTypefaceHandler)face.Handler).CreateFont(size); - this.decoration = decoration; + _decoration = decoration; } public void Create(SystemFont systemFont, float? fontSize, FontDecoration decoration) @@ -134,7 +135,7 @@ public void Create(SystemFont systemFont, float? fontSize, FontDecoration decora default: throw new NotSupportedException(); } - this.decoration = decoration; + _decoration = decoration; } #if OSX @@ -173,9 +174,9 @@ public static NSFont CreateFont(string familyName, nfloat size, NSFontTraitMask public void Create(FontFamily family, float size, FontStyle style, FontDecoration decoration) { - this.style = style; - this.family = family; - this.decoration = decoration; + _style = style; + _family = family; + _decoration = decoration; #if OSX var familyHandler = (FontFamilyHandler)family.Handler; traits = style.ToNS() & familyHandler.TraitMask; @@ -217,9 +218,9 @@ public FontFamily Family { get { - if (family == null) - family = new FontFamily(new FontFamilyHandler(Control.FamilyName)); - return family; + if (_family == null) + _family = new FontFamily(new FontFamilyHandler(Control.FamilyName)); + return _family; } } @@ -227,13 +228,13 @@ public FontTypeface Typeface { get { - if (typeface == null) + if (_typeface == null) #if IOS typeface = ((FontFamilyHandler)Family.Handler).GetFace(Control); #else - typeface = ((FontFamilyHandler)Family.Handler).GetFace(Control, traits); + _typeface = ((FontFamilyHandler)Family.Handler).GetFace(Control, traits); #endif - return typeface; + return _typeface; } } @@ -241,17 +242,17 @@ public FontStyle FontStyle { get { - if (style == null) + if (_style == null) #if OSX - style = NSFontManager.SharedFontManager.TraitsOfFont(Control).ToEto(); + _style = NSFontManager.SharedFontManager.TraitsOfFont(Control).ToEto(); #elif IOS style = Typeface.FontStyle; #endif - return style.Value; + return _style.Value; } } - public FontDecoration FontDecoration => decoration; + public FontDecoration FontDecoration => _decoration; public float Ascent => (float)Control.Ascender; @@ -307,8 +308,8 @@ NSDictionary CreateAttributes() new NSObject[] { Control ?? NSFont.UserFontOfSize(Size), - new NSNumber((int)(decoration.HasFlag(FontDecoration.Underline) ? NSUnderlineStyle.Single : NSUnderlineStyle.None)), - NSNumber.FromBoolean(decoration.HasFlag(FontDecoration.Strikethrough)) + new NSNumber((int)(_decoration.HasFlag(FontDecoration.Underline) ? NSUnderlineStyle.Single : NSUnderlineStyle.None)), + NSNumber.FromBoolean(_decoration.HasFlag(FontDecoration.Strikethrough)) }, attributeKeys ); diff --git a/src/Eto.Mac/Drawing/FontTypefaceHandler.cs b/src/Eto.Mac/Drawing/FontTypefaceHandler.cs index cec200af0c..ca20c4b88a 100644 --- a/src/Eto.Mac/Drawing/FontTypefaceHandler.cs +++ b/src/Eto.Mac/Drawing/FontTypefaceHandler.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Collections.Generic; using Eto.Forms; +using System.IO; #if XAMMAC2 @@ -27,6 +28,7 @@ namespace Eto.Mac.Drawing public class FontTypefaceHandler : WidgetHandler, FontTypeface.IHandler { NSFont _font; + CGFont _cgfont; string _name; static readonly object LocalizedName_Key = new object(); @@ -59,11 +61,21 @@ public FontTypefaceHandler(NSFont font, NSFontTraitMask? traits = null) public FontTypefaceHandler(string postScriptName, string name, NSFontTraitMask traits, int weight) { PostScriptName = postScriptName; - this._name = name; + _name = name; Weight = weight; Traits = traits; } + public FontTypefaceHandler() + { + } + + public FontTypefaceHandler(CGFont cgfont, string faceName) + { + _cgfont = cgfont; + _name = faceName; + } + internal static NSString GetName(IntPtr fontHandle) { var namePtr = CTFontCopyName(fontHandle, CTFontNameKeySubFamily.Handle); @@ -93,7 +105,7 @@ public string Name } [DllImport("/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreText.framework/CoreText")] - static extern IntPtr CTFontCopyName(IntPtr font, IntPtr nameKey); + internal static extern IntPtr CTFontCopyName(IntPtr font, IntPtr nameKey); [DllImport("/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreText.framework/CoreText")] static extern IntPtr CTFontCopyLocalizedName(IntPtr font, IntPtr nameKey, out IntPtr actualLanguage); @@ -120,9 +132,15 @@ string GetLocalizedName() public bool IsSymbol => Font.FontDescriptor.SymbolicTraits.HasFlag(NSFontSymbolicTraits.SymbolicClass); + public FontFamily Family { get; private set; } + public NSFont CreateFont(float size) { - + if (_cgfont != null) + { + var ctfont = new CTFont(_cgfont, size, null); + return new NSFont(ctfont.Handle); + } // we have a postcript name, use that to create the font if (!string.IsNullOrEmpty(PostScriptName)) { @@ -155,6 +173,34 @@ public bool HasCharacterRanges(IEnumerable> ranges) } return true; } + + public void Create(Stream stream) + { + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + var bytes = ms.ToArray(); + using (var dataProvider = new CGDataProvider(bytes, 0, bytes.Length)) + { + _cgfont = CGFont.CreateFromProvider(dataProvider); + } + } + Family = new FontFamily(new FontFamilyHandler(_cgfont, this)); + } + + public void Create(string fileName) + { + using (var dataProvider = new CGDataProvider(fileName)) + { + _cgfont = CGFont.CreateFromProvider(dataProvider); + } + Family = new FontFamily( new FontFamilyHandler(_cgfont, this)); + } + + public void Create(FontFamily family) + { + Family = family; + } } } diff --git a/src/Eto.Mac/Platform.cs b/src/Eto.Mac/Platform.cs index 6d54024ed1..7224aea28e 100644 --- a/src/Eto.Mac/Platform.cs +++ b/src/Eto.Mac/Platform.cs @@ -162,6 +162,7 @@ public static void AddTo(Eto.Platform p) // Drawing p.Add(() => new BitmapHandler()); p.Add(() => new FontFamilyHandler()); + p.Add(() => new FontTypefaceHandler()); p.Add(() => new FontHandler()); p.Add(() => new FontsHandler()); p.Add(() => new GraphicsHandler()); diff --git a/src/Eto.WinForms/Drawing/FontFamilyHandler.cs b/src/Eto.WinForms/Drawing/FontFamilyHandler.cs index b137e74704..91c06e945f 100644 --- a/src/Eto.WinForms/Drawing/FontFamilyHandler.cs +++ b/src/Eto.WinForms/Drawing/FontFamilyHandler.cs @@ -2,17 +2,19 @@ using System.Collections.Generic; using sd = System.Drawing; using System.Globalization; +using System.Linq; +using System.IO; +using System; +using Eto.Shared.Drawing; namespace Eto.WinForms.Drawing { public class FontFamilyHandler : WidgetHandler, FontFamily.IHandler { - public string Name { get; set; } + string _name; + public string Name => _name ?? (_name = Control.GetName(0)); - public string LocalizedName - { - get { return Control.Name; } - } + public string LocalizedName => Control.Name; static IEnumerable Styles { @@ -25,31 +27,73 @@ static IEnumerable Styles } } - public IEnumerable Typefaces + FontTypeface[] _typefaces; + + public IEnumerable Typefaces => _typefaces ?? (_typefaces = GetTypefaces().ToArray()); + + IEnumerable GetTypefaces() { - get + if (FontsHandler.UseTypographicFonts) { - foreach (var style in Styles) + var searchName = Name; + switch (Name.ToUpperInvariant()) + { + case FontFamilies.MonospaceFamilyName: + searchName = sd.FontFamily.GenericMonospace.Name; + break; + case FontFamilies.SansFamilyName: + searchName = sd.FontFamily.GenericSansSerif.Name; + break; + case FontFamilies.SerifFamilyName: + searchName = sd.FontFamily.GenericSerif.Name; + break; + case FontFamilies.CursiveFamilyName: + searchName = "Comic Sans MS"; + break; + case FontFamilies.FantasyFamilyName: + searchName = "Gabriola"; + break; + } + + var family = Eto.Drawing.Fonts.AvailableFontFamilies.FirstOrDefault(r => r.Name == searchName); + if (family == null) + yield break; + + foreach (var typeface in family.Typefaces) { - if (Control.IsStyleAvailable(style)) - yield return new FontTypeface(Widget, new FontTypefaceHandler(style)); + yield return typeface; } + yield break; + } + foreach (var style in Styles) + { + if (Control.IsStyleAvailable(style)) + yield return new FontTypeface(Widget, new FontTypefaceHandler(Control, style)); } } + internal void SetTypefaces(FontTypeface[] typefaces) + { + _typefaces = typefaces; + } + public FontFamilyHandler() { } - public FontFamilyHandler(sd.FontFamily windowsFamily) + public FontFamilyHandler(sd.FontFamily windowsFamily, string name = null) { - this.Control = windowsFamily; - Name = Control.GetName(0); + Control = windowsFamily; + _name = name; + if (_name == null && FontsHandler.UseTypographicFonts) + { + _name = FontsHandler.FindFontFamilyName(Control); + } } public void Create(string familyName) { - Name = familyName; + _name = familyName; switch (familyName.ToUpperInvariant()) { case FontFamilies.MonospaceFamilyName: @@ -69,9 +113,152 @@ public void Create(string familyName) break; default: Control = new sd.FontFamily(familyName); - Name = Control.GetName(0); + _name = Control.GetName(0); break; } } + + static string[] defaultVariationNames = new[] { "Bold", "Italic", "Regular" }; + + internal static FontFamily CreateFamily(string familyName, IEnumerable typefaceInfos) + { + var familyHandler = new FontFamilyHandler(null, familyName); + var family = new FontFamily(familyHandler); + var typefaces = new List(); + + void AddTypeface(OpenTypeFontInfo typefaceInfo, string sdfamilyName, string variationName) + { + try + { + var sdfamily = new sd.FontFamily(sdfamilyName); + if (!familyHandler.HasControl) + familyHandler.Control = sdfamily; + var typefaceHandler = new FontTypefaceHandler(sdfamily, typefaceInfo, variationName); + var typeface = new FontTypeface(family, typefaceHandler); + typefaces.Add(typeface); + } + catch (ArgumentException) + { + } + } + + foreach (var typefaceInfo in typefaceInfos) + { + if (typefaceInfo.VariationSubFamilyNames?.Length > 0) + { + // variable font, combine with variations + var gdinames = typefaceInfo.VariationSubFamilyNames.Select(r => GetGdiCompatibleName(typefaceInfo, r)).ToList(); + foreach (var name in gdinames) + { + AddTypeface(typefaceInfo, name.familyName, name.typefaceName); + } + } + else + { + AddTypeface(typefaceInfo, typefaceInfo.FamilyName, null); + } + } + if (typefaces.Count == 0) + return null; + + familyHandler.SetTypefaces(typefaces.ToArray()); + return family; + } + + private static (string familyName, string typefaceName) GetGdiCompatibleName(OpenTypeFontInfo typefaceInfo, string variationName) + { + var sdfamilyName = typefaceInfo.TypographicFamilyName ?? typefaceInfo.FamilyName; + + // turn variation names to GDI+ compatible name + var subfamilies = variationName.Split(' ') + .OrderBy(VariationSorter).ToList(); + + var subfamilyName = string.Join(" ", subfamilies.Where(r => Array.IndexOf(defaultVariationNames, r) == -1)); + var typefaceName = string.Join(" ", subfamilies); + + if (!string.IsNullOrEmpty(subfamilyName)) + sdfamilyName += " " + subfamilyName; + + return (sdfamilyName, typefaceName); + } + + private static int VariationSorter(string arg) + { + // GDI+ orders the variations in a consistent way it seems.. + if (arg.IndexOf("regular", StringComparison.OrdinalIgnoreCase) >= 0) + return -1; + if (arg.IndexOf("bold", StringComparison.OrdinalIgnoreCase) >= 0) + return 2; + if (arg.IndexOf("light", StringComparison.OrdinalIgnoreCase) >= 0) + return 3; + if (arg.IndexOf("condensed", StringComparison.OrdinalIgnoreCase) >= 0) + return 4; + return 0; + } + + internal void SetFontCollection(sd.Text.PrivateFontCollection fontCollection) => _fontCollection = fontCollection; + + sd.Text.PrivateFontCollection _fontCollection; + + public void CreateFromFiles(IEnumerable fileNames) + { + _fontCollection = new sd.Text.PrivateFontCollection(); + var infos = new List(); + foreach (var fileName in fileNames) + { + _fontCollection.AddFontFile(fileName); + infos.AddRange(OpenTypeFontInfo.FromFile(fileName)); + } + + // map to typographic family/typefaces + MapToTypographic(infos); + } + + private void MapToTypographic(List infos) + { + var typefaces = new List(); + var families = _fontCollection.Families; + for (int i = 0; i < infos.Count; i++) + { + var info = infos[i]; + var currentName = info.TypographicFamilyName ?? info.FamilyName; + if (_name == null) + _name = currentName; + else if (_name != currentName) + throw new InvalidOperationException($"Family name of the supplied font files do not match. '{_name}' and '{currentName}'"); + + var family = families.FirstOrDefault(r => r.Name == info.FamilyName); + if (family == null) + family = families.First(); + + var typefaceHandler = new FontTypefaceHandler(family, info); + var typeface = new FontTypeface(Widget, typefaceHandler); + typefaces.Add(typeface); + } + _typefaces = typefaces.ToArray(); + Control = _fontCollection.Families[0]; + } + + public unsafe void CreateFromStreams(IEnumerable streams) + { + _fontCollection = new sd.Text.PrivateFontCollection(); + var infos = new List(); + foreach (var stream in streams) + { + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + + var bytes = ms.ToArray(); + fixed (byte* ptr = bytes) + { + _fontCollection.AddMemoryFont((IntPtr)ptr, bytes.Length); + } + ms.Position = 0; + infos.AddRange(OpenTypeFontInfo.FromStream(ms)); + } + } + MapToTypographic(infos); + } } } diff --git a/src/Eto.WinForms/Drawing/FontHandler.cs b/src/Eto.WinForms/Drawing/FontHandler.cs index ea1de630a2..4109ff9cec 100644 --- a/src/Eto.WinForms/Drawing/FontHandler.cs +++ b/src/Eto.WinForms/Drawing/FontHandler.cs @@ -13,8 +13,8 @@ public interface IWindowsFontSource public class FontHandler : WidgetHandler, Font.IHandler, IWindowsFontSource { - FontTypeface typeface; - FontFamily family; + FontTypeface _typeface; + FontFamily _family; public FontHandler() { @@ -29,17 +29,17 @@ public FontHandler(sd.Font font) public void Create(FontFamily family, float size, FontStyle style, FontDecoration decoration) { - this.family = family; + _family = family; var familyHandler = (FontFamilyHandler)family.Handler; Control = new sd.Font(familyHandler.Control, size, style.ToSD() | decoration.ToSD()); } public void Create(FontTypeface typeface, float size, FontDecoration decoration) { - this.typeface = typeface; - - var familyHandler = (FontFamilyHandler)typeface.Family.Handler; - Control = new sd.Font(familyHandler.Control, size, typeface.FontStyle.ToSD() | decoration.ToSD()); + _typeface = typeface; + var typefaceHandler = (FontTypefaceHandler)typeface.Handler; + _family = typefaceHandler.Family; + Control = new sd.Font(typefaceHandler.SDFontFamily, size, typefaceHandler.Control | decoration.ToSD()); } public void Create(SystemFont systemFont, float? size, FontDecoration decoration) @@ -52,50 +52,23 @@ public void Create(SystemFont systemFont, float? size, FontDecoration decoration } } - public string FamilyName - { - get { return Control.FontFamily.Name; } - } + public string FamilyName => Control.FontFamily.Name; - public FontStyle FontStyle - { - get { return Control.Style.ToEtoStyle(); } - } + public FontStyle FontStyle => Control.Style.ToEtoStyle(); - public FontDecoration FontDecoration - { - get { return Control.Style.ToEtoDecoration(); } - } + public FontDecoration FontDecoration => Control.Style.ToEtoDecoration(); - public FontFamily Family - { - get { return family = family ?? new FontFamily(new FontFamilyHandler(Control.FontFamily)); } - } + public FontFamily Family => _family ?? (_family = new FontFamily(new FontFamilyHandler(Control.FontFamily))); - public FontTypeface Typeface - { - get { return typeface = typeface ?? new FontTypeface(Family, new FontTypefaceHandler(Control.Style)); } - } + public FontTypeface Typeface => _typeface ?? (_typeface = new FontTypeface(Family, new FontTypefaceHandler(Control.FontFamily, Control.Style))); - public sd.FontFamily WindowsFamily - { - get { return Control.FontFamily; } - } + public sd.FontFamily WindowsFamily => Control.FontFamily; - public float XHeight - { - get { return Size * 0.5f; } - } + public float XHeight => Size * 0.5f; - public float Baseline - { - get { return Ascent; } - } + public float Baseline => Ascent; - public float Leading - { - get { return LineHeight - (Ascent + Descent); } - } + public float Leading => LineHeight - (Ascent + Descent); float? ascent; public float Ascent @@ -123,14 +96,11 @@ public float Descent } } - public float LineHeight { get { return Size * Control.FontFamily.GetLineSpacing(Control.Style) / Control.FontFamily.GetEmHeight(Control.Style); } } + public float LineHeight => Size * Control.FontFamily.GetLineSpacing(Control.Style) / Control.FontFamily.GetEmHeight(Control.Style); - public float Size { get { return Control.SizeInPoints; } } + public float Size => Control.SizeInPoints; - public sd.Font GetFont() - { - return Control; - } + public sd.Font GetFont() => Control; [DefaultValue(true)] public bool UseCompatibleTextRendering { get; set; } diff --git a/src/Eto.WinForms/Drawing/FontTypefaceHandler.cs b/src/Eto.WinForms/Drawing/FontTypefaceHandler.cs index d2d0049d00..2c6350c8c0 100755 --- a/src/Eto.WinForms/Drawing/FontTypefaceHandler.cs +++ b/src/Eto.WinForms/Drawing/FontTypefaceHandler.cs @@ -1,26 +1,48 @@ using Eto.Drawing; using Eto.Forms; +using Eto.Shared.Drawing; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; +using System.Text; using sd = System.Drawing; +using sdt = System.Drawing.Text; namespace Eto.WinForms.Drawing { public class FontTypefaceHandler : WidgetHandler, FontTypeface.IHandler { - string name; + string _name; bool? _isSymbol; sd.Font _font; List _fontRanges; - FontFamilyHandler FamilyHandler => (FontFamilyHandler)Widget.Family.Handler; + sd.FontFamily _sdfamily; + public FontFamilyHandler FamilyHandler => (FontFamilyHandler)Widget.Family.Handler; - public FontTypefaceHandler(sd.FontStyle style) + public FontTypefaceHandler(sd.FontFamily sdfamily, sd.FontStyle style) { + _sdfamily = sdfamily; Control = style; + if (FontsHandler.UseTypographicFonts) + { + _name = FontsHandler.FindFontTypefaceName(_sdfamily, Control); + } + } + + internal FontTypefaceHandler(sd.FontFamily sdfamily, OpenTypeFontInfo info, string variationName = null) + { + _sdfamily = sdfamily; + _name = variationName ?? info.TypographicSubFamilyName ?? info.SubFamilyName; + SetFontStyle(info.SubFamilyName); + } + + public FontTypefaceHandler() + { } - public string Name => name ?? (name = GetName()); + public string Name => _name ?? (_name = GetName()); public string LocalizedName => Name; @@ -35,10 +57,14 @@ bool GetIsSymbol() } sd.Font Font => _font ?? (_font = GetFont()); - sd.Font GetFont() => new sd.Font(FamilyHandler.Control, 1, Control); + sd.Font GetFont() => new sd.Font(SDFontFamily, 1, Control); + + public sd.FontFamily SDFontFamily => _sdfamily ?? FamilyHandler.Control; List FontRanges => _fontRanges ?? (_fontRanges = Font.GetUnicodeRangesForFont()); + public FontFamily Family { get; private set; } + public bool HasCharacterRanges(IEnumerable> ranges) { var supportedRanges = FontRanges; @@ -65,6 +91,77 @@ public bool HasCharacterRanges(IEnumerable> ranges) return true; } - string GetName() => FontStyle.ToString().Replace(",", string.Empty); + string GetName() => Control.ToString().Replace(",", string.Empty); + + void SetFontStyle(string subFamilyName) + { + if (subFamilyName == null) + return; + + if (subFamilyName.IndexOf("italic", StringComparison.OrdinalIgnoreCase) >= 0) + Control |= sd.FontStyle.Italic; + if (subFamilyName.IndexOf("bold", StringComparison.OrdinalIgnoreCase) >= 0) + Control |= sd.FontStyle.Bold; + } + + public unsafe void Create(Stream stream) + { + var fontCollection = new sdt.PrivateFontCollection(); + OpenTypeFontInfo fontInfo = null; + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + + var bytes = ms.ToArray(); + fixed (byte* ptr = bytes) + { + fontCollection.AddMemoryFont((IntPtr)ptr, bytes.Length); + } + ms.Position = 0; + fontInfo = OpenTypeFontInfo.FromStream(ms).Single(); + } + + var families = fontCollection.Families; + + if (families.Length == 0) + throw new ArgumentOutOfRangeException(nameof(stream), "Could not load font from stream"); + + + _name = fontInfo?.TypographicSubFamilyName ?? fontInfo?.SubFamilyName; + SetFontStyle(fontInfo?.SubFamilyName); + + var sdfamily = families[0]; + var familyHandler = new FontFamilyHandler(sdfamily); + familyHandler.SetFontCollection(fontCollection); + familyHandler.SetTypefaces(new[] { Widget }); + Family = new FontFamily(familyHandler); + + } + + public void Create(string fileName) + { + var fontCollection = new sdt.PrivateFontCollection(); + fontCollection.AddFontFile(fileName); + var families = fontCollection.Families; + + if (families.Length == 0) + throw new ArgumentOutOfRangeException(nameof(fileName), "Could not load font from file"); + + + var fontInfo = OpenTypeFontInfo.FromFile(fileName).Single(); + _name = fontInfo?.TypographicSubFamilyName ?? fontInfo?.SubFamilyName; + SetFontStyle(fontInfo?.SubFamilyName); + + var sdfamily = families[0]; + var familyHandler = new FontFamilyHandler(sdfamily); + familyHandler.SetFontCollection(fontCollection); + familyHandler.SetTypefaces(new[] { Widget }); + Family = new FontFamily(familyHandler); + } + + public void Create(FontFamily family) + { + Family = family; + } } } diff --git a/src/Eto.WinForms/Drawing/FontsHandler.cs b/src/Eto.WinForms/Drawing/FontsHandler.cs index d4dc19d41e..97fda8348b 100644 --- a/src/Eto.WinForms/Drawing/FontsHandler.cs +++ b/src/Eto.WinForms/Drawing/FontsHandler.cs @@ -1,6 +1,9 @@ using Eto.Drawing; +using Eto.Shared.Drawing; +using Microsoft.Win32; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using sd = System.Drawing; @@ -9,23 +12,116 @@ namespace Eto.WinForms.Drawing public class FontsHandler : WidgetHandler, Fonts.IHandler { HashSet availableFontFamilies; + static FontFamily[] _families; - public IEnumerable AvailableFontFamilies + /// + /// Setting to use typographic family/sub-family names vs. the old style which uses FontStyle as the typeface names. + /// Note this is disabled for now as it doesn't properly deal with variable fonts or localized names. + /// + public static bool UseTypographicFonts { get; set; } = false; + + public IEnumerable AvailableFontFamilies => _families ?? (_families = GetFontFamilies().ToArray()); + + public static IEnumerable GetFontFamilies() + { + if (UseTypographicFonts) + return GetTypographicFontFamilies(); + else + return GetNormalFontFamilies(); + } + + public static IEnumerable GetNormalFontFamilies() + { + return sd.FontFamily.Families.Select(r => new FontFamily(new FontFamilyHandler(r))); + } + + internal static string FindFontFamilyName(sd.FontFamily sdfamily) + { + _families = _families ?? (_families = GetFontFamilies().ToArray()); + var familyName = sdfamily.Name; + foreach (var family in _families.Select(r => r.Handler).OfType()) + { + if (!familyName.StartsWith(family.Name, StringComparison.OrdinalIgnoreCase)) + continue; + + foreach (var typeface in family.Typefaces.Select(r => r.Handler).OfType()) + { + if (typeface.SDFontFamily.Name == familyName) + { + return family.Name; + } + } + } + return null; + } + + internal static string FindFontTypefaceName(sd.FontFamily sdfamily, sd.FontStyle fontStyle) + { + _families = _families ?? (_families = GetFontFamilies().ToArray()); + var familyName = sdfamily.Name; + foreach (var family in _families.Select(r => r.Handler).OfType()) + { + if (!familyName.StartsWith(family.Name, StringComparison.OrdinalIgnoreCase)) + continue; + + foreach (var typeface in family.Typefaces.Select(r => r.Handler).OfType()) + { + if (typeface.SDFontFamily.Name == familyName && typeface.Control == fontStyle) + { + return typeface.Name; + } + } + } + return null; + } + + public static IEnumerable GetTypographicFontFamilies() + { + var fontInfos = GetInstalledFontFiles().SelectMany(OpenTypeFontInfo.FromFile); + var familyInfos = fontInfos.Where(r => r != null).GroupBy(r => r.TypographicFamilyName ?? r.FamilyName); + foreach (var familyInfo in familyInfos) + { + var family = FontFamilyHandler.CreateFamily(familyInfo.Key, familyInfo); + if (family == null) + continue; + yield return family; + } + } + + public static IEnumerable GetInstalledFontFiles() { - get { - return sd.FontFamily.Families.Select (r => new FontFamily(new FontFamilyHandler(r))); + var fontsFolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); + + using (var installedFonts = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion\Fonts")) + { + if (installedFonts != null) + { + foreach (var installedFont in installedFonts.GetValueNames()) + { + var filename = installedFonts.GetValue(installedFont) as string; + if (string.IsNullOrWhiteSpace(filename)) + continue; + if (!Path.IsPathRooted(filename)) + filename = Path.Combine(fontsFolder, filename); + if (File.Exists(filename)) + yield return filename; + } + } } } - public bool FontFamilyAvailable (string fontFamily) + + public bool FontFamilyAvailable(string fontFamily) { - if (availableFontFamilies == null) { - availableFontFamilies = new HashSet (StringComparer.InvariantCultureIgnoreCase); - foreach (var family in sd.FontFamily.Families) { - availableFontFamilies.Add (family.Name); + if (availableFontFamilies == null) + { + availableFontFamilies = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (var family in AvailableFontFamilies) + { + availableFontFamilies.Add(family.Name); } } - return availableFontFamilies.Contains (fontFamily); + return availableFontFamilies.Contains(fontFamily); } } } diff --git a/src/Eto.WinForms/Eto.WinForms.csproj b/src/Eto.WinForms/Eto.WinForms.csproj index b60a0bbdd4..5cccaa9690 100755 --- a/src/Eto.WinForms/Eto.WinForms.csproj +++ b/src/Eto.WinForms/Eto.WinForms.csproj @@ -94,6 +94,9 @@ You do not need to use any of the classes of this assembly (unless customizing t Drawing\BaseBitmapData.cs + + Drawing\OpenTypeFontInfo.cs + Forms\TaskbarHandler.cs diff --git a/src/Eto.WinForms/Forms/Controls/RichTextAreaHandler.cs b/src/Eto.WinForms/Forms/Controls/RichTextAreaHandler.cs index 73efc9c589..209f618009 100644 --- a/src/Eto.WinForms/Forms/Controls/RichTextAreaHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/RichTextAreaHandler.cs @@ -41,7 +41,11 @@ void SetSelectionFontStyle(Func setFont) if (sel.Length() > 0) SetFontStyle(sel, setFont); else - Control.SelectionFont = setFont(Control.SelectionFont); + { + var sdfont = setFont(Control.SelectionFont); + if (sdfont != null) + Control.SelectionFont = sdfont; + } } void SetFontStyle(Range range, Func setFont) @@ -66,7 +70,9 @@ void SetFontStyle(Range range, Func setFont) { // at end, set font on last range Control.Select(lastPosition, i - lastPosition + 1); - Control.SelectionFont = setFont(font); + var sdfont = setFont(font); + if (sdfont != null) + Control.SelectionFont = sdfont; } currentFont = font; } @@ -135,7 +141,7 @@ public FontFamily SelectionFamily public FontTypeface SelectionTypeface { - get => SelectionFont.Typeface; + get => SelectionFont?.Typeface; set { SetSelectionFontStyle(font => value.ToSDFont(font.Size)); diff --git a/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs b/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs index d33ea9b75b..fe4ab7799b 100644 --- a/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs @@ -50,6 +50,8 @@ public class TextAreaHandler : WindowsControl true; + public static Size DefaultMinimumSize = new Size(100, 60); public override Size? GetDefaultSize(Size availableSize) diff --git a/src/Eto.WinForms/Forms/WindowsControl.cs b/src/Eto.WinForms/Forms/WindowsControl.cs index 6bbba17696..7d36e77845 100644 --- a/src/Eto.WinForms/Forms/WindowsControl.cs +++ b/src/Eto.WinForms/Forms/WindowsControl.cs @@ -12,7 +12,7 @@ namespace Eto.WinForms.Forms { - public interface IWindowsControl: Control.IHandler + public interface IWindowsControl : Control.IHandler { bool InternalVisible { get; } @@ -115,11 +115,11 @@ public abstract class WindowsControl : WidgetHandl // used in DrawableHandler public class PanelBase : swf.Panel - where THandler: WindowsControl + where THandler : WindowsControl { public THandler Handler { get; set; } - public PanelBase( THandler handler = null ) + public PanelBase(THandler handler = null) { Handler = handler; Size = sd.Size.Empty; @@ -162,26 +162,26 @@ protected override bool IsInputKey(swf.Keys keyData) public class EtoPanel : PanelBase where THandler : WindowsControl { - public EtoPanel( THandler handler = null ) - : base( handler ) + public EtoPanel(THandler handler = null) + : base(handler) { } // optimization especially for content on drawable - protected override void OnBackColorChanged( EventArgs e ) + protected override void OnBackColorChanged(EventArgs e) { SetStyle - ( swf.ControlStyles.AllPaintingInWmPaint + (swf.ControlStyles.AllPaintingInWmPaint | swf.ControlStyles.DoubleBuffer - , BackColor.A != 255 ); - base.OnBackColorChanged( e ); + , BackColor.A != 255); + base.OnBackColorChanged(e); } - protected override void OnParentBackColorChanged( EventArgs e ) + protected override void OnParentBackColorChanged(EventArgs e) { SetStyle - ( swf.ControlStyles.AllPaintingInWmPaint + (swf.ControlStyles.AllPaintingInWmPaint | swf.ControlStyles.DoubleBuffer - , BackColor.A != 255 ); - base.OnParentBackColorChanged( e ); + , BackColor.A != 255); + base.OnParentBackColorChanged(e); } } @@ -544,7 +544,8 @@ public virtual string Text public virtual Size Size { - get { + get + { if (!Widget.Loaded) return UserPreferredSize; return ContainerControl.Size.ToEto(); @@ -581,7 +582,7 @@ public virtual int Height protected virtual void SetAutoSize() { - ContainerControl.AutoSize = + ContainerControl.AutoSize = (UserPreferredSize.Width == -1 || UserPreferredSize.Height == -1) && (UserDesiredClientSize.Width == -1 || UserDesiredClientSize.Height == -1); } @@ -661,11 +662,12 @@ public virtual Color BackgroundColor } } bool backgroundColorSet; - public bool BackgroundColorSet { - get { return backgroundColorSet; } + public bool BackgroundColorSet + { + get { return backgroundColorSet; } set { - if (!( backgroundColorSet = value )) + if (!(backgroundColorSet = value)) Control.BackColor = sd.Color.Empty; } } @@ -875,6 +877,8 @@ void Control_TextChanged(object sender, EventArgs e) } } + internal virtual bool SetFontTwiceForSomeReason => false; + public Font Font { get @@ -884,7 +888,10 @@ public Font Font set { Widget.Properties[WindowsControl.FontKey] = value; - Control.Font = value.ToSD(); + var sdfont = value.ToSD(); + Control.Font = sdfont; + if (SetFontTwiceForSomeReason) + Control.Font = sdfont; } } @@ -1008,7 +1015,7 @@ public void DoDragDrop(DataObject data, DragEffects allowedEffects, Image image, public void Print() { - + } } } diff --git a/src/Eto.WinForms/Platform.cs b/src/Eto.WinForms/Platform.cs index 90b6adb045..e65f3a7c68 100644 --- a/src/Eto.WinForms/Platform.cs +++ b/src/Eto.WinForms/Platform.cs @@ -46,6 +46,7 @@ public static void AddTo(Eto.Platform p) // Drawing p.Add(() => new BitmapHandler()); p.Add(() => new FontFamilyHandler()); + p.Add(() => new FontTypefaceHandler()); p.Add(() => new FontHandler()); p.Add(() => new FontsHandler()); p.Add(() => new GraphicsHandler()); diff --git a/src/Eto.WinForms/WinConversions.cs b/src/Eto.WinForms/WinConversions.cs index b0403e8484..4d1ed5dd6b 100644 --- a/src/Eto.WinForms/WinConversions.cs +++ b/src/Eto.WinForms/WinConversions.cs @@ -211,12 +211,17 @@ public static sd.FontFamily ToSD(this FontFamily family) public static sd.FontStyle ToSD(this FontTypeface typeface) { + if (typeface == null) + return sd.FontStyle.Regular; return FontTypefaceHandler.GetControl(typeface); } public static sd.Font ToSDFont(this FontTypeface typeface, float size) { - return new sd.Font(typeface.Family.ToSD(), size, typeface.ToSD()); + var typefaceHandler = typeface?.Handler as FontTypefaceHandler; + if (typefaceHandler == null) + return null; + return new sd.Font(typefaceHandler.SDFontFamily, size, typeface.ToSD()); } public static sd.Font ToSD(this SystemFont systemFont) diff --git a/src/Eto.Wpf/Drawing/FontFamilyHandler.cs b/src/Eto.Wpf/Drawing/FontFamilyHandler.cs index d42336047e..a7b80dea46 100755 --- a/src/Eto.Wpf/Drawing/FontFamilyHandler.cs +++ b/src/Eto.Wpf/Drawing/FontFamilyHandler.cs @@ -7,51 +7,65 @@ using System.Diagnostics; using System.Globalization; using Eto.Forms; +using System.IO; +using System; +using Eto.Shared.Drawing; +using Eto.Wpf.CustomControls.FontDialog; namespace Eto.Wpf.Drawing { public class FontFamilyHandler : WidgetHandler, FontFamily.IHandler { - public FontFamilyHandler () + FontTypeface[] _typefaces; + + ~FontFamilyHandler() + { + Dispose(false); + } + + public FontFamilyHandler() { } public FontFamilyHandler(swm.FontFamily wpfFamily) { Control = wpfFamily; - var familyMapName = Control.FamilyNames.Select(r => r.Value).FirstOrDefault(); + var familyMapName = NameDictionaryExtensions.GetEnglishName(Control.FamilyNames); Name = familyMapName ?? Control.Source; } + internal void SetTypefaces(FontTypeface[] typefaces) => _typefaces = typefaces; + public FontFamilyHandler(swd.TextSelection range, sw.Controls.RichTextBox control) { Control = range.GetPropertyValue(swd.TextElement.FontFamilyProperty) as swm.FontFamily ?? swd.TextElement.GetFontFamily(control); - var familyMapName = Control.FamilyNames.Select(r => r.Value).FirstOrDefault(); + var familyMapName = NameDictionaryExtensions.GetEnglishName(Control.FamilyNames); Name = familyMapName ?? Control.Source; } - public void Create (string familyName) + public void Create(string familyName) { Name = familyName; - switch (familyName.ToUpperInvariant ()) { - case FontFamilies.MonospaceFamilyName: - familyName = "Courier New"; - break; - case FontFamilies.SansFamilyName: - familyName = "Tahoma, Arial, Verdana, Trebuchet, MS Sans Serif, Helvetica"; - break; - case FontFamilies.SerifFamilyName: - familyName = "Times New Roman"; - break; - case FontFamilies.CursiveFamilyName: - familyName = "Comic Sans MS, Monotype Corsiva, Papryus"; - break; - case FontFamilies.FantasyFamilyName: - familyName = "Impact, Juice ITC"; - break; + switch (familyName.ToUpperInvariant()) + { + case FontFamilies.MonospaceFamilyName: + familyName = "Courier New"; + break; + case FontFamilies.SansFamilyName: + familyName = "Tahoma, Arial, Verdana, Trebuchet, MS Sans Serif, Helvetica"; + break; + case FontFamilies.SerifFamilyName: + familyName = "Times New Roman"; + break; + case FontFamilies.CursiveFamilyName: + familyName = "Comic Sans MS, Monotype Corsiva, Papryus"; + break; + case FontFamilies.FantasyFamilyName: + familyName = "Impact, Juice ITC"; + break; } - Control = new swm.FontFamily (familyName); + Control = new swm.FontFamily(familyName); } public string Name { get; set; } @@ -72,14 +86,15 @@ public string LocalizedName } } - public IEnumerable Typefaces + public IEnumerable Typefaces => _typefaces ?? (_typefaces = GetTypefaces().ToArray()); + + IEnumerable GetTypefaces() { - get { - foreach (var type in Control.GetTypefaces ()) { - if (!FontHandler.ShowSimulatedFonts && (type.IsBoldSimulated || type.IsObliqueSimulated)) - continue; - yield return new FontTypeface(Widget, new FontTypefaceHandler (type)); - } + foreach (var type in Control.GetTypefaces()) + { + if (!FontHandler.ShowSimulatedFonts && (type.IsBoldSimulated || type.IsObliqueSimulated)) + continue; + yield return new FontTypeface(Widget, new FontTypefaceHandler(type)); } } @@ -87,5 +102,109 @@ public void Apply(sw.Documents.TextRange control) { control.ApplyPropertyValue(swd.TextElement.FontFamilyProperty, Control); } + + + public void CreateFromFiles(IEnumerable fileNames) + { + // add to private font collection + string fontPath = null; + + bool useFallback = false; + foreach (var fileName in fileNames) + { + var currentPath = Path.GetDirectoryName(fileName); + if (fontPath == null) + fontPath = currentPath; + else if (fontPath != currentPath) + throw new InvalidOperationException("All fonts in the family must be in the same directory."); + + var fontInfos = OpenTypeFontInfo.FromFile(fileName); + foreach (var fontInfo in fontInfos) + { + if (fontInfo == null) + { + // for some reason can't read info from this file.. fallback to dumb way. + useFallback = true; + Name = null; + } + if (!useFallback) + { + var currentName = fontInfo.TypographicFamilyName ?? fontInfo.FamilyName; + if (Name == null) + Name = currentName; + else if (Name != currentName) + throw new InvalidOperationException($"Family name of the supplied font files do not match. '{Name}' and '{currentName}'"); + } + } + } + + if (useFallback) + { + // do we need this?? + // get the font family name using System.Drawing + + var fontCollection = new System.Drawing.Text.PrivateFontCollection(); + foreach (var fileName in fileNames) + { + fontCollection.AddFontFile(fileName); + } + + var families = fontCollection.Families; + var shortest = families.OrderBy(r => r.Name.Length).First(); + if (!families.All(r => r.Name.StartsWith(shortest.Name))) + { + const string RegularSuffix = " Regular"; + var regular = families.Where(r => r.Name.EndsWith(RegularSuffix)).FirstOrDefault(); + if (regular != null) + { + var familyName = regular.Name.Substring(regular.Name.Length - RegularSuffix.Length); + if (!families.All(r => r.Name.StartsWith(familyName))) + { + throw new ArgumentException("Fonts must all be in the same family"); + } + Name = familyName; + } + } + else + Name = shortest.Name; + } + var name = $"file:///{fontPath.Replace("\\", "/")}/#{Name}"; + Control = new swm.FontFamily(name); + } + + string fontTempDirectory; + + public void CreateFromStreams(IEnumerable streams) + { + fontTempDirectory = FontTypefaceHandler.CreateTempDirectoryForFonts(); + var fileNames = new List(); + foreach (var stream in streams) + { + // assume everything is an otf so WPF picks it up.. We should detect the format somehow. :/ + var fileName = Path.Combine(fontTempDirectory, Guid.NewGuid().ToString() + ".otf"); + fileNames.Add(fileName); + using (var fs = File.Create(fileName)) + { + stream.CopyTo(fs); + } + } + CreateFromFiles(fileNames); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (fontTempDirectory != null && Directory.Exists(fontTempDirectory)) + { + try + { + Directory.Delete(fontTempDirectory, true); + } + catch + { + } + fontTempDirectory = null; + } + } } } diff --git a/src/Eto.Wpf/Drawing/FontTypefaceHandler.cs b/src/Eto.Wpf/Drawing/FontTypefaceHandler.cs index 7e74f49b4d..a09edc2902 100755 --- a/src/Eto.Wpf/Drawing/FontTypefaceHandler.cs +++ b/src/Eto.Wpf/Drawing/FontTypefaceHandler.cs @@ -7,19 +7,32 @@ using Eto.Wpf.CustomControls.FontDialog; using System.Collections.Generic; using Eto.Forms; +using System.IO; +using System.Runtime.InteropServices; +using System; +using Eto.Shared.Drawing; namespace Eto.Wpf.Drawing { public class FontTypefaceHandler : WidgetHandler, FontTypeface.IHandler { - string name; - string localizedName; + string _name; + string _localizedName; - public FontTypefaceHandler (swm.Typeface type) + ~FontTypefaceHandler() + { + Dispose(false); + } + + public FontTypefaceHandler(swm.Typeface type) { this.Control = type; } + public FontTypefaceHandler() + { + } + public FontTypefaceHandler(swd.TextSelection range, sw.Controls.RichTextBox control) { var family = range.GetPropertyValue(swd.TextElement.FontFamilyProperty) as swm.FontFamily ?? swd.TextElement.GetFontFamily(control); @@ -37,15 +50,17 @@ public void Apply(swd.TextRange range) range.ApplyPropertyValue(swd.TextElement.FontWeightProperty, Control.Weight); } - public string Name => name ?? (name = NameDictionaryExtensions.GetEnglishName(Control.FaceNames)); + public string Name => _name ?? (_name = NameDictionaryExtensions.GetEnglishName(Control.FaceNames)); - public string LocalizedName => localizedName ?? (localizedName = NameDictionaryExtensions.GetDisplayName(Control.FaceNames)); + public string LocalizedName => _localizedName ?? (_localizedName = NameDictionaryExtensions.GetDisplayName(Control.FaceNames)); - public FontStyle FontStyle => WpfConversions.Convert (Control.Style, Control.Weight); + public FontStyle FontStyle => WpfConversions.Convert(Control.Style, Control.Weight); public bool IsSymbol => Control.TryGetGlyphTypeface(out var glyph) && glyph.Symbol; + public FontFamily Family { get; private set; } + public bool HasCharacterRanges(IEnumerable> ranges) { if (Control.TryGetGlyphTypeface(out var glyph)) @@ -61,6 +76,91 @@ public bool HasCharacterRanges(IEnumerable> ranges) return true; } + + internal static string CreateTempDirectoryForFonts() + { + var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + return dir; + } + + string tempFontDirectory; + + public void Create(Stream stream) + { + tempFontDirectory = CreateTempDirectoryForFonts(); + // assume everything is an otf so WPF picks it up.. We should detect the format somehow. :/ + var fileName = Path.Combine(tempFontDirectory, Guid.NewGuid().ToString() + ".otf"); + + // Ideally we'd load this from memory somehow like you can when you compile resources + // however those type of resources can't be added dynamically as far as I can tell. + // However, apparently there's a huge memory leak when doing so anyway, + // so we create a file instead. + // https://stackoverflow.com/questions/31452443/wpf-textblock-memory-leak-when-using-font + var fileStream = File.Create(fileName); + stream.CopyTo(fileStream); + fileStream.Close(); + + Create(fileName); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (tempFontDirectory != null && Directory.Exists(tempFontDirectory)) + { + try + { + Directory.Delete(tempFontDirectory); + } + catch + { + } + tempFontDirectory = null; + } + } + + public void Create(string fileName) + { + var fontInfo = OpenTypeFontInfo.FromFile(fileName).Single(); + string familyName = null; + string faceName = null; + if (fontInfo != null) + { + familyName = fontInfo.TypographicFamilyName ?? fontInfo.FamilyName; + faceName = fontInfo.TypographicSubFamilyName ?? fontInfo.SubFamilyName; + } + else + { + // do we need this?? + // get the font family name using System.Drawing.. + var fontCollection = new System.Drawing.Text.PrivateFontCollection(); + fontCollection.AddFontFile(fileName); + var families = fontCollection.Families; + if (families.Length == 0) + throw new ArgumentOutOfRangeException(nameof(fileName), "Could not load font from file"); + familyName = families[0].Name; + } + + // is there no way to use fonts other than from a file system?!? ugh. + var path = Path.GetDirectoryName(fileName); + var name = $"file:///{path.Replace("\\", "/")}/#{familyName}"; + var wpffamily = new swm.FontFamily(name); + var familyHandler = new FontFamilyHandler(wpffamily); + familyHandler.SetTypefaces(new[] { Widget }); + Family = new FontFamily(familyHandler); + if (faceName != null) + Control = wpffamily.GetTypefaces().First(r => NameDictionaryExtensions.GetEnglishName(r.FaceNames) == faceName); + else + Control = wpffamily.GetTypefaces().First(); + } + public void Create(FontFamily family) + { + Family = family; + } } } diff --git a/src/Eto.Wpf/Eto.Wpf.csproj b/src/Eto.Wpf/Eto.Wpf.csproj index 8c80223847..4fa39df5d5 100755 --- a/src/Eto.Wpf/Eto.Wpf.csproj +++ b/src/Eto.Wpf/Eto.Wpf.csproj @@ -123,6 +123,9 @@ You do not need to use any of the classes of this assembly (unless customizing t Drawing\BaseBitmapData.cs + + Drawing\OpenTypeFontInfo.cs + diff --git a/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs b/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs index 5fef59e462..43a88146e5 100755 --- a/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs @@ -434,6 +434,8 @@ public virtual FontTypeface SelectionTypeface set { var typeface = (value?.Handler as FontTypefaceHandler)?.Control; + if (typeface == null) + return; ApplyFont(OnTranslateFamily(typeface?.FontFamily ?? Control.FontFamily), OnTranslateTypeface(typeface), typeface?.Weight, typeface?.Stretch, typeface?.Style); } } diff --git a/src/Eto.Wpf/Platform.cs b/src/Eto.Wpf/Platform.cs index bd4f968c3b..8f90db320e 100755 --- a/src/Eto.Wpf/Platform.cs +++ b/src/Eto.Wpf/Platform.cs @@ -64,6 +64,7 @@ public static void AddTo(Eto.Platform p) // Drawing p.Add(() => new BitmapHandler()); p.Add(() => new FontFamilyHandler()); + p.Add(() => new FontTypefaceHandler()); p.Add(() => new FontHandler()); p.Add(() => new FontsHandler()); p.Add(() => new GraphicsHandler()); diff --git a/src/Eto/Drawing/Font.cs b/src/Eto/Drawing/Font.cs index 60e7d716a4..5eb3dd0197 100644 --- a/src/Eto/Drawing/Font.cs +++ b/src/Eto/Drawing/Font.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.IO; using sc = System.ComponentModel; namespace Eto.Drawing @@ -126,7 +127,41 @@ public enum FontDecoration [Handler(typeof(Font.IHandler))] public class Font : Widget { - new IHandler Handler { get { return (IHandler)base.Handler; } } + new IHandler Handler => (IHandler)base.Handler; + + /// + /// Creates a new instance of the Font class with the specified font file on disk. + /// + /// + /// Note that calling this multiple times for the same file may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Path to the font file to load + /// Size of the font, in points + /// Docorations to apply to the font + /// A new instance of the Font object + /// + public static Font FromFile(string fileName, float size, FontDecoration decoration = FontDecoration.None) + { + return new Font(new FontTypeface(fileName), size, decoration); + } + + /// + /// Creates a new instance of the Font class from the specified stream. + /// + /// + /// Note that calling this multiple times for the same stream may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Stream to a font file to load + /// Size of the font, in points + /// Decorations to apply to the font + /// A new instance of the Font object + /// + public static Font FromStream(Stream stream, float size, FontDecoration decoration = FontDecoration.None) + { + return new Font(new FontTypeface(stream), size, decoration); + } /// /// Creates a new instance of the Font class with a specified , , and @@ -193,7 +228,7 @@ public Font(IHandler handler) : base(handler) { } - + /// /// Gets the name of the family of this font /// diff --git a/src/Eto/Drawing/FontFamily.cs b/src/Eto/Drawing/FontFamily.cs index 91c0a950d3..4cbf42a472 100755 --- a/src/Eto/Drawing/FontFamily.cs +++ b/src/Eto/Drawing/FontFamily.cs @@ -1,6 +1,7 @@ using Eto.Forms; using System; using System.Collections.Generic; +using System.IO; namespace Eto.Drawing { @@ -47,6 +48,59 @@ public class FontFamily : Widget, IEquatable /// public IEnumerable Typefaces => Handler.Typefaces; + + /// + /// Creates a new instance of the FontFamily class with the specified font files. + /// + /// + /// All font files specified must be a part of the same typographical font family. + /// + /// Also, calling this multiple times for the same streams may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Path to the file(s) to include in this font family + /// A new instance of the FontFamily class + public static FontFamily FromFiles(params string[] fileNames) => new FontFamily(fileNames); + + /// + /// Creates a new instance of the FontFamily class with the specified font files. + /// + /// + /// All font files specified must be a part of the same typographical font family. + /// + /// Also, calling this multiple times for the same streams may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Path to the file(s) to include in this font family + /// A new instance of the FontFamily class + public static FontFamily FromFiles(IEnumerable fileNames) => new FontFamily(fileNames); + + /// + /// Creates a new instance of the FontFamily class with the specified font file streams. + /// + /// + /// All font files specified must be a part of the same typographical font family. + /// + /// Also, calling this multiple times for the same streams may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Streams of the font file(s) to include in this font family + /// A new instance of the FontFamily class + public static FontFamily FromStreams(params Stream[] streams) => new FontFamily(streams); + + /// + /// Creates a new instance of the FontFamily class with the specified font file streams. + /// + /// + /// All font files specified must be a part of the same typographical font family. + /// + /// Also, calling this multiple times for the same streams may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Streams of the font file(s) to include in this font family + /// A new instance of the FontFamily class + public static FontFamily FromStreams(IEnumerable streams) => new FontFamily(streams); + /// /// Initializes a new instance of the FontFamily class with the specified handler /// @@ -57,6 +111,7 @@ public class FontFamily : Widget, IEquatable public FontFamily(IHandler handler) : base(handler) { + Initialize(); } /// @@ -69,6 +124,19 @@ public FontFamily(string familyName) familyName = SplitFamilyName(familyName); Handler.Create(familyName); + Initialize(); + } + + private FontFamily(IEnumerable fileNames) + { + Handler.CreateFromFiles(fileNames); + Initialize(); + } + + private FontFamily(IEnumerable streams) + { + Handler.CreateFromStreams(streams); + Initialize(); } static string SplitFamilyName(string familyName) @@ -158,6 +226,7 @@ static string SplitFamilyName(string familyName) /// /// Interface for a handler /// + [AutoInitialize(false)] public new interface IHandler : Widget.IHandler { /// @@ -190,6 +259,18 @@ static string SplitFamilyName(string familyName) /// /// Name of the font family to create this instance for void Create(string familyName); + + /// + /// Creates a new instance of a font family with the given file names. + /// + /// Paths to the font files to include in this FontFamily + void CreateFromFiles(IEnumerable fileNames); + + /// + /// Creates a new instance of a font family with the given font file streams. + /// + /// Streams to each of the font files to include in this FontFamily + void CreateFromStreams(IEnumerable streams); } } } diff --git a/src/Eto/Drawing/FontTypeface.cs b/src/Eto/Drawing/FontTypeface.cs index e1a473abf7..a65f85c06a 100755 --- a/src/Eto/Drawing/FontTypeface.cs +++ b/src/Eto/Drawing/FontTypeface.cs @@ -1,6 +1,8 @@ using Eto.Forms; +using System; using System.Collections.Generic; +using System.IO; namespace Eto.Drawing { @@ -15,6 +17,7 @@ namespace Eto.Drawing /// /// (c) 2014 by Curtis Wensley /// See LICENSE for full terms + [Handler(typeof(IHandler))] public class FontTypeface : Widget { new IHandler Handler => (IHandler)base.Handler; @@ -22,7 +25,7 @@ public class FontTypeface : Widget /// /// Gets the family of this typeface /// - public FontFamily Family { get; private set; } + public FontFamily Family => Handler.Family; /// /// Gets the name of this typeface @@ -73,7 +76,38 @@ public class FontTypeface : Widget public FontTypeface(FontFamily family, IHandler handler) : base(handler) { - Family = family; + Handler.Create(family); + Initialize(); + } + + /// + /// Creates a new instance of the FontTypeface from the specified font file on disk. + /// + /// + /// Note that calling this multiple times for the same file may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Path to the font file to load + /// + public FontTypeface(string fileName) + { + Handler.Create(fileName); + Initialize(); + } + + /// + /// Creates a new instance of the FontTypeface from a font in the specified stream. + /// + /// + /// Note that calling this multiple times for the same stream may cause additional overhead or unpredictable results, + /// so you should keep a copy of it in memory when you want to use it. + /// + /// Stream to a font file to load + /// + public FontTypeface(Stream stream) + { + Handler.Create(stream); + Initialize(); } /// @@ -107,7 +141,7 @@ public FontTypeface(FontFamily family, IHandler handler) return true; if (ReferenceEquals(value1, null) || ReferenceEquals(value2, null)) return false; - return value1.Name == value2.Name; + return value1.Family == value2.Family && value1.Name == value2.Name; } /// @@ -166,11 +200,12 @@ public override int GetHashCode() /// Ranges to test /// True if the font supports all characters in the specified ranges, false otherwise public bool HasCharacterRanges(IEnumerable> ranges) => Handler.HasCharacterRanges(ranges); - - + + /// /// Platform handler interface for the class /// + [AutoInitialize(false)] public new interface IHandler : Widget.IHandler { /// @@ -198,6 +233,35 @@ public override int GetHashCode() /// Gets a value indicating that this font is a symbol font and not generally used for text /// bool IsSymbol { get; } + + /// + /// Gets the FontFamily associated with this typeface + /// + FontFamily Family { get; } + + /// + /// Called when creating a FontTypeface for the specified FontFamily + /// + /// FontFamily this typeface was created for + void Create(FontFamily family); + + /// + /// Called when creating a FontTypeface for the specified font file stream. + /// + /// + /// Note that the Family must not be null after this call. + /// + /// Stream of the font file to load in this typeface + void Create(Stream stream); + + /// + /// Called when creating a FontTypeface for the specified font file name. + /// + /// + /// Note that the Family must not be null after this call. + /// + /// Path to the font file to load in this typeface + void Create(string fileName); /// /// Gets a value indicating that this font supports the character ranges specified diff --git a/src/Shared/OpenTypeFontInfo.cs b/src/Shared/OpenTypeFontInfo.cs new file mode 100755 index 0000000000..fa9cf1242c --- /dev/null +++ b/src/Shared/OpenTypeFontInfo.cs @@ -0,0 +1,339 @@ +using System.IO; +using System; +using System.Text; +using System.Collections.Generic; +using System.Linq; + +namespace Eto.Shared.Drawing +{ + /// + /// Gets the opentype typographic font name/sub family info from file, since there's no built-in APIs + /// to do that in win32 or gdi. + /// Reference: https://docs.microsoft.com/en-ca/typography/opentype/spec/otff + /// + class OpenTypeFontInfo + { + public string FamilyName { get; private set; } + public string SubFamilyName { get; private set; } + public string TypographicFamilyName { get; private set; } + public string TypographicSubFamilyName { get; private set; } + public string[] VariationSubFamilyNames { get; private set; } + // public string[] VariationPostscriptNames { get; private set; } + + public static IEnumerable FromFile(string fontFilePath) + { + using (var stream = File.OpenRead(fontFilePath)) + { + // enumerate everything so the file isn't closed while we are reading. + return FromStream(stream).ToList(); + } + } + + public static IEnumerable FromStream(Stream stream) + { + if (OTTTCHeader.TryRead(stream, out var ttcHeader)) + { + foreach (var tableDirectory in ttcHeader.ReadTableDirectories(stream)) + { + yield return ReadFont(stream); + } + } + else + yield return ReadFont(stream); + } + + static OpenTypeFontInfo ReadFont(Stream stream) + { + // https://docs.microsoft.com/en-ca/typography/opentype/spec/otff#table-directory + var offsetTable = new OTTableDirectory(stream); + + // validate the versions we know about in the spec + if (!(offsetTable.sfntVersion == 0x00010000 + || offsetTable.sfntVersion == 0x4F54544F + )) + return null; + + // find the name table + var tableLookup = offsetTable.ReadRecords(stream).ToDictionary(r => r.TagName); + + // no name table.. bad font? + if (!tableLookup.TryGetValue("name", out var nameTableRecord)) + return null; + + var info = new OpenTypeFontInfo(); + + // get all the name table records to extract the font family name + // https://docs.microsoft.com/en-ca/typography/opentype/spec/name + var nameTableHeader = new OTNamingTableHeader(nameTableRecord, stream); + var nameRecords = nameTableHeader.ReadRecords(stream) + .Where(r => r.platformID == 0 || r.platformID == 3) + .ToLookup(r => r.nameID); + + string GetNameValue(UInt16 nameID) => nameRecords[nameID].FirstOrDefault()?.GetString(stream); + + // https://docs.microsoft.com/en-ca/typography/opentype/spec/name#name-ids + info.FamilyName = GetNameValue(1); + info.SubFamilyName = GetNameValue(2); + info.TypographicFamilyName = GetNameValue(16); + info.TypographicSubFamilyName = GetNameValue(17); + + if (tableLookup.TryGetValue("fvar", out var fontVariationsTable)) + { + // variable font, so get the variations + var variableHeader = new OTFontVariationsHeader(fontVariationsTable, stream); + var variations = variableHeader.ReadInstanceRecords(stream); + info.VariationSubFamilyNames = variations.Select(r => GetNameValue(r.subfamilyNameID)).ToArray(); + // info.VariationPostscriptNames = variations.Where(r => r.postScriptNameID != 0xFFFF).Select(r => GetNameValue(r.postScriptNameID)).ToArray(); + } + + return info; + } + + static byte[] bufferUInt16 = new byte[2]; + + static UInt16 ReadUInt16(Stream stream) + { + stream.Read(bufferUInt16, 0, bufferUInt16.Length); + Array.Reverse(bufferUInt16); + return BitConverter.ToUInt16(bufferUInt16, 0); + } + + static byte[] bufferUInt32 = new byte[4]; + static unsafe UInt32 ReadUInt32(Stream stream) + { + stream.Read(bufferUInt32, 0, bufferUInt32.Length); + Array.Reverse(bufferUInt32); + return BitConverter.ToUInt32(bufferUInt32, 0); + } + + class OTTTCHeader + { + public byte[] ttcTag; + public UInt16 majorVersion; + public UInt16 minorVersion; + public UInt32 numFonts; + + static byte[] ttcTagIdentifier = new[] { (byte)'t', (byte)'t', (byte)'c', (byte)'f' }; + + public static bool TryRead(Stream stream, out OTTTCHeader header) + { + var pos = stream.Position; + var ttcTag = new byte[4]; + stream.Read(ttcTag, 0, ttcTag.Length); + if (!ttcTag.SequenceEqual(ttcTagIdentifier)) + { + stream.Position = pos; + header = null; + return false; + } + + header = new OTTTCHeader(); + header.ttcTag = ttcTag; + header.majorVersion = ReadUInt16(stream); + header.minorVersion = ReadUInt16(stream); + header.numFonts = ReadUInt32(stream); + return true; + } + + public IEnumerable ReadTableDirectories(Stream stream) + { + var currentPos = stream.Position; + for (int i = 0; i < numFonts; i++) + { + stream.Position = currentPos; + var offset = ReadUInt32(stream); + currentPos = stream.Position; + stream.Position = offset; + var tableDirectory = new OTTableDirectory(stream); + yield return tableDirectory; + } + } + } + + class OTTableDirectory + { + public UInt32 sfntVersion; + public UInt16 numTables; + public UInt16 searchRange; + public UInt16 entrySelector; + public UInt16 rangeShift; + + public OTTableDirectory(Stream stream) + { + sfntVersion = ReadUInt32(stream); + numTables = ReadUInt16(stream); + searchRange = ReadUInt16(stream); + entrySelector = ReadUInt16(stream); + rangeShift = ReadUInt16(stream); + } + + public IEnumerable ReadRecords(Stream stream) + { + var currentPos = stream.Position; + for (int i = 0; i <= numTables; i++) + { + stream.Position = currentPos; + var tableRecord = new OTTableRecord(stream); + // remember position for next record so we can do other things + currentPos = stream.Position; + yield return tableRecord; + } + } + } + class OTTableRecord + { + public byte[] tableTag = new byte[4]; + public UInt32 checksum; + public UInt32 offset; + public UInt32 length; + + string _tagName; + + public string TagName => _tagName ?? (_tagName = Encoding.UTF8.GetString(tableTag)); + + public OTTableRecord(Stream stream) + { + stream.Read(tableTag, 0, tableTag.Length); + checksum = ReadUInt32(stream); + offset = ReadUInt32(stream); + length = ReadUInt32(stream); + + } + } + class OTNamingTableHeader + { + public UInt16 version; + public UInt16 count; + public UInt16 storageOffset; + internal long absoluteOffset; + + internal OTNamingTableHeader(OTTableRecord nameTableRecord, Stream stream) + { + stream.Position = nameTableRecord.offset; + version = ReadUInt16(stream); + count = ReadUInt16(stream); + storageOffset = ReadUInt16(stream); + absoluteOffset = nameTableRecord.offset + storageOffset; + } + + internal IEnumerable ReadRecords(Stream stream) + { + var currentPos = stream.Position; + for (int i = 0; i < count; i++) + { + stream.Position = currentPos; + var nameRecord = new OTNameRecord(this, stream); + // remember position for next record so we can do other things + currentPos = stream.Position; + yield return nameRecord; + } + } + } + + class OTNameRecord + { + public UInt16 platformID; + public UInt16 encodingID; + public UInt16 languageID; + public UInt16 nameID; + public UInt16 length; + public UInt16 stringOffset; + long absoluteOffset; + + public OTNameRecord(OTNamingTableHeader nameTableHeader, Stream stream) + { + platformID = ReadUInt16(stream); + encodingID = ReadUInt16(stream); + languageID = ReadUInt16(stream); + nameID = ReadUInt16(stream); + length = ReadUInt16(stream); + stringOffset = ReadUInt16(stream); + + absoluteOffset = nameTableHeader.absoluteOffset + stringOffset; + } + + public string GetString(Stream stream) + { + stream.Position = absoluteOffset; + + var stringBuffer = new byte[length]; + stream.Read(stringBuffer, 0, length); + + var isBigEndian = platformID == 3 || (platformID == 0 && encodingID == 1 || encodingID == 3); + var encoding = isBigEndian ? Encoding.BigEndianUnicode : Encoding.UTF8; + + return encoding.GetString(stringBuffer); + } + } + + class OTFontVariationsHeader + { + public UInt16 majorVersion; + public UInt16 minorVersion; + public UInt16 axesArrayOffset; + public UInt16 reserved; + public UInt16 axisCount; + public UInt16 axisSize; + public UInt16 instanceCount; + public UInt16 instanceSize; + + long _recordsOffset; + + public OTFontVariationsHeader(OTTableRecord tableRecord, Stream stream) + { + stream.Position = tableRecord.offset; + majorVersion = ReadUInt16(stream); + minorVersion = ReadUInt16(stream); + axesArrayOffset = ReadUInt16(stream); + reserved = ReadUInt16(stream); + axisCount = ReadUInt16(stream); + axisSize = ReadUInt16(stream); + instanceCount = ReadUInt16(stream); + instanceSize = ReadUInt16(stream); + _recordsOffset = stream.Position; + } + + public IEnumerable ReadInstanceRecords(Stream stream) + { + var pos = _recordsOffset + axisCount * axisSize; + bool includePostscriptName = instanceSize >= axisCount * 32 + 6; + + for (int i = 0; i < instanceCount; i++) + { + stream.Position = pos + i * instanceSize; + yield return new OTFontVariationsInstanceRecord(stream, axisCount, includePostscriptName); + } + } + } + + class OTFontVariationsInstanceRecord + { + public UInt16 subfamilyNameID; + public UInt16 flags; + // public UInt32[] coordinates; // Fixed point 16.16 + public UInt16 postScriptNameID; + + public OTFontVariationsInstanceRecord(Stream stream, int axisCount, bool includePostscriptName) + { + subfamilyNameID = ReadUInt16(stream); + flags = ReadUInt16(stream); + + // don't need to actually read the coordinates for now + // coordinates = new UInt32[axisCount]; + // for (int i = 0; i < axisCount; i++) + // { + // coordinates[i] = ReadUInt32(stream); + // } + if (includePostscriptName) + { + stream.Position += 32 * axisCount; + postScriptNameID = ReadUInt16(stream); + } + else + postScriptNameID = 0xFFFF; + } + } + } + + +} diff --git a/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs b/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs index 9dcce10a98..433ecff4b6 100644 --- a/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs +++ b/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs @@ -86,14 +86,6 @@ public RichTextAreaSection() }; var typefaceDropDown = new DropDown(); - typefaceDropDown.ItemKeyBinding = Binding.Property((FontTypeface f) => f.Name); - typefaceDropDown.DataStore = richText.SelectionFamily.Typefaces; - var tyepfaceBinding = typefaceDropDown.SelectedValueBinding.Bind(richText, r => r.SelectionTypeface); - typefaceDropDown.SelectedValueChanged += (sender, e) => - { - richText.Focus(); - UpdateBindings(BindingUpdateMode.Destination); - }; var familyDropDown = new DropDown(); familyDropDown.ItemTextBinding = Binding.Property((FontFamily f) => f.LocalizedName); @@ -109,6 +101,15 @@ public RichTextAreaSection() UpdateBindings(BindingUpdateMode.Destination); }; + typefaceDropDown.ItemKeyBinding = Binding.Property((FontTypeface f) => f.Name); + typefaceDropDown.DataStore = richText.SelectionFamily.Typefaces; + var tyepfaceBinding = typefaceDropDown.SelectedValueBinding.Bind(richText, r => r.SelectionTypeface); + typefaceDropDown.SelectedValueChanged += (sender, e) => + { + richText.Focus(); + UpdateBindings(BindingUpdateMode.Destination); + }; + var formatEnum = new EnumDropDown(); formatEnum.SelectedValue = RichTextAreaFormat.Rtf; diff --git a/test/Eto.Test/Sections/Dialogs/FontDialogSection.cs b/test/Eto.Test/Sections/Dialogs/FontDialogSection.cs index ae36bcfa10..b8d2f3edbd 100644 --- a/test/Eto.Test/Sections/Dialogs/FontDialogSection.cs +++ b/test/Eto.Test/Sections/Dialogs/FontDialogSection.cs @@ -3,6 +3,7 @@ using Eto.Forms; using System.Collections.Generic; using System.Linq; +using System.IO; namespace Eto.Test.Sections.Dialogs { @@ -11,6 +12,8 @@ public class FontDialogSection : Scrollable { Font selectedFont; TextArea preview; + Label labelPreview; + TextBox textBoxPreview; ListBox fontList; ListBox fontStyles; ListBox fontSizes; @@ -21,14 +24,29 @@ public FontDialogSection() { var layout = new DynamicLayout { DefaultSpacing = new Size(5, 5), Padding = new Padding(10) }; - layout.AddSeparateRow(null, PickFont(), PickFontWithStartingFont(), SetToFontFamily(), null); + layout.BeginVertical(); + layout.BeginHorizontal(); + layout.AddSpace(); + layout.Add(PickFont()); + layout.Add(PickFontWithStartingFont()); + layout.Add(SetToFontFamily()); + if (Platform.Supports()) + { + layout.Add(FromFilesButton()); + layout.Add(FromStreamsButton()); + } + layout.AddSpace(); + layout.EndHorizontal(); + layout.EndVertical(); layout.AddSeparateRow(null, new Label { Text = "Set Font Family", VerticalAlignment = VerticalAlignment.Center }, PickFontFamily(), null); layout.AddSeparateRow(null, FontList(), FontStyles(), FontSizes(), null); layout.AddSeparateRow(null, "Style:", BoldFont(), ItalicFont(), UnderlineFont(), StrikeoutFont(), null); var tabs = new TabControl(); - tabs.Pages.Add(new TabPage { Text = "Preview", Content = Preview() }); + tabs.Pages.Add(new TabPage { Text = "TextArea Preview", Content = Preview() }); + tabs.Pages.Add(new TabPage { Text = "TextBox Preview", Content = TextBoxPreview() }); + tabs.Pages.Add(new TabPage { Text = "Label Preview", Content = LabelPreview() }); tabs.Pages.Add(new TabPage { Text = "Metrics", Content = Metrics() }); layout.Add(new Panel { MinimumSize = new Size(100, 150), Content = tabs }, yscale: true); @@ -37,6 +55,102 @@ public FontDialogSection() Content = layout; } + Control FromFilesButton() + { + var button = new Button { Text = "FromFile(s)" }; + button.Click += (sender, e) => + { + var ofd = new OpenFileDialog + { + CheckFileExists = true, + MultiSelect = true, + Filters = { + new FileFilter("Font File", ".ttf", ".otf") + } + }; + if (ofd.ShowDialog(this) == DialogResult.Ok) + { + Application.Instance.AsyncInvoke(() => + { + try + { + var files = ofd.Filenames.ToArray(); + Font font; + if (files.Length == 1) + { + font = Font.FromFile(ofd.FileName, selectedFont.Size, selectedFont.FontDecoration); + } + else + { + var family = FontFamily.FromFiles(files); + var typeface = family.Typefaces.First(); + font = new Font(typeface, selectedFont.Size, selectedFont.FontDecoration); + } + UpdatePreview(font, true); + } + catch (Exception ex) + { + Log.Write(this, $"Could not load font: {ex}"); + } + }); + } + }; + + return button; + } + + Control FromStreamsButton() + { + var button = new Button { Text = "FromStreams(s)" }; + button.Click += (sender, e) => + { + var ofd = new OpenFileDialog + { + CheckFileExists = true, + MultiSelect = true, + Filters = { + new FileFilter("Font File", ".ttf", ".otf") + } + }; + if (ofd.ShowDialog(this) == DialogResult.Ok) + { + Application.Instance.AsyncInvoke(() => + { + try + { + Font font; + var files = ofd.Filenames.ToArray(); + if (files.Length == 1) + { + using (var stream = File.OpenRead(ofd.FileName)) + { + font = Font.FromStream(stream, selectedFont.Size, selectedFont.FontDecoration); + } + } + else + { + var streams = files.Select(File.OpenRead).ToArray(); + var family = FontFamily.FromStreams(streams); + foreach (var stream in streams) + { + stream.Dispose(); + } + var typeface = family.Typefaces.First(); + font = new Font(typeface, selectedFont.Size, selectedFont.FontDecoration); + } + UpdatePreview(font, true); + } + catch (Exception ex) + { + Log.Write(this, $"Could not load font: {ex}"); + } + }); + } + }; + + return button; + } + Control PickFontFamily() { var fontFamilyName = new TextBox { Text = "Times, serif", Size = new Size(200, -1) }; @@ -153,7 +267,7 @@ Control PickFontWithStartingFont() Control SetToFontFamily() { - var button = new Button { Text = "Set to a specific font family (Times New Roman 20pt)" }; + var button = new Button { Text = "Set to Times New Roman, 20pt" }; button.Click += delegate { var family = new FontFamily("Times New Roman"); @@ -270,7 +384,7 @@ Control StrikeoutFont() return control; } - void UpdatePreview(Font font) + void UpdatePreview(Font font, bool forceNewFamily = false) { if (updating) return; @@ -279,21 +393,24 @@ void UpdatePreview(Font font) selectedFont = font; DataContext = selectedFont; preview.Font = selectedFont; + labelPreview.Font = selectedFont; + textBoxPreview.Font = selectedFont; preview.Invalidate(); var family = selectedFont.Family; - if (newFamily) + if (newFamily || forceNewFamily) { fontStyles.Items.Clear(); - Func getFaceName = (FontTypeface ft) => + string getFaceName(FontTypeface ft) { var name = ft.Name; if (ft.LocalizedName != name) name += $" ({ft.LocalizedName})"; return name; - }; + } fontStyles.Items.AddRange(family.Typefaces.Select(r => new ListItem { Text = getFaceName(r), Key = r.Name }).OfType()); + Log.Write(null, $"New Family: {family.Name}, Typefaces: {string.Join(", ", family.Typefaces.Select(getFaceName))}"); } fontStyles.SelectedKey = selectedFont.Typeface.Name; fontList.SelectedKey = family.Name; @@ -373,6 +490,20 @@ Control Preview() return preview; } + Control TextBoxPreview() + { + textBoxPreview = new TextBox(); + textBoxPreview.Text = "The quick brown fox jumps over the lazy dog"; + + return new TableLayout(textBoxPreview, null); + } + Control LabelPreview() + { + labelPreview = new Label { Width = 100 }; + labelPreview.Text = "The quick brown fox jumps over the lazy dog"; + + return labelPreview; + } } }