public const string kInvalid = "invalid"; // bad file format (eg, .ttc) /// <summary> /// On Window, we can use System.Windows.Media (which provides the GlyphTypeface class) to /// provide all the font metadata information. /// On Linux, we have to use Sharpfont from nuget (which provides the Sharpfont.Face class) /// for reading the font's embedding flag plus running /usr/bin/otfino for everything else. /// We get /usr/bin/otfinfo as part of the lcdf-typetools package that is specified in the /// debian/control file. /// </summary> public FontMetadata(string fontName, FontGroup group) { name = fontName; fileExtension = Path.GetExtension(group.Normal); // First we detect invalid font types that we don't know how to handle (like .ttc) var bloomKnows = fontFileTypesBloomKnows.Contains(fileExtension.ToLowerInvariant()); if (!bloomKnows) { determinedSuitability = kInvalid; determinedSuitabilityNotes = "Bloom does not support " + fileExtension + " fonts."; return; } #if __MonoCS__ try { using (var lib = new SharpFont.Library()) { using (var face = new Face(lib, group.Normal)) { var embeddingFlags = face.GetFSTypeFlags(); /* * Quoting from the standard (https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fstype) * Valid fonts must set at most one of bits 1, 2 or 3; bit 0 is permanently reserved and must be zero. * Valid values for this sub-field are 0, 2, 4 or 8. The meaning of these values is as follows: * 0: Installable embedding * 2: Restricted License embedding * 4: Preview & Print embedding * 8: Editable embedding * * The specification for versions 0 to 2 did not specify that bits 0 to 3 must be mutually exclusive. * Rather, those specifications stated that, in the event that more than one of bits 0 to 3 are set in * a given font, then the least-restrictive permission indicated take precedence. * So we pay attention to RestrictedLicense only if neither Editable nor PreviewAndPrint is set. (This * is apparently what Microsoft does in the GlyphTypeFace implementation.) */ if ((embeddingFlags & (EmbeddingTypes.RestrictedLicense | EmbeddingTypes.Editable | EmbeddingTypes.PreviewAndPrint)) == EmbeddingTypes.RestrictedLicense) { fsType = "Restricted License"; } else if ((embeddingFlags & EmbeddingTypes.BitmapOnly) == EmbeddingTypes.BitmapOnly) { fsType = "Bitmaps Only"; } else if ((embeddingFlags & EmbeddingTypes.Editable) == EmbeddingTypes.Editable) { fsType = "Editable"; } else if ((embeddingFlags & EmbeddingTypes.PreviewAndPrint) == EmbeddingTypes.PreviewAndPrint) { fsType = "Print and preview"; } else { fsType = "Installable"; // but is it really? } // Every call to face.GetSfntName(i) throws a null object exception. // Otherwise the code would build on this fragment. //var count = face.GetSfntNameCount(); //for (uint i = 0; i < count; ++i) //{ // try // { // var sfntName = face.GetSfntName(i); // ... // } // catch (Exception ex) // { // } //} } } } catch (Exception ex) { Console.WriteLine("SharpLib threw an exception for {0}: {1}", group.Normal, ex); determinedSuitability = kInvalid; determinedSuitabilityNotes = $"SharpLib exception: {ex}"; return; } try { var process = new Process { StartInfo = new ProcessStartInfo() { FileName = "/usr/bin/otfinfo", Arguments = $"-i \"{group.Normal}\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, }, }; process.Start(); process.WaitForExit(); var standardOutput = process.StandardOutput.ReadToEnd(); var standardError = process.StandardError.ReadToEnd(); if (process.ExitCode == 0 && standardError.Length == 0) { ParseOtfinfoOutput(standardOutput); } else { Console.WriteLine("otfinfo -i \"{0}\" returned {1}. Standard Error =\n{2}", group.Normal, process.ExitCode, standardError); determinedSuitability = kInvalid; determinedSuitabilityNotes = $"otfinfo returned: {process.ExitCode}. Standard Error={Environment.NewLine}{standardError}"; return; } } catch (Exception e) { Console.WriteLine("Process.Start() of otfinfo -i \"{0}\" threw an exception: {1}", group.Normal, e); determinedSuitability = kInvalid; determinedSuitabilityNotes = $"Process.Start of \"otfinfo\" exception: {e}"; return; } #else GlyphTypeface gtf = null; try { gtf = new GlyphTypeface(new Uri("file:///" + group.Normal)); var english = System.Globalization.CultureInfo.GetCultureInfo("en-US"); version = gtf.VersionStrings[english]; // Most fonts include the text "Version x" here, but our UI provides the // (possibly localized) text, so we strip it out here. if (version.StartsWith("Version ")) { version = version.Replace("Version ", ""); } copyright = gtf.Copyrights[english]; var embeddingRights = gtf.EmbeddingRights; switch (embeddingRights) { case FontEmbeddingRight.Editable: case FontEmbeddingRight.EditableButNoSubsetting: fsType = "Editable"; break; case FontEmbeddingRight.Installable: case FontEmbeddingRight.InstallableButNoSubsetting: fsType = "Installable"; break; case FontEmbeddingRight.PreviewAndPrint: case FontEmbeddingRight.PreviewAndPrintButNoSubsetting: fsType = "Print and preview"; break; case FontEmbeddingRight.RestrictedLicense: fsType = "Restricted License"; break; case FontEmbeddingRight.EditableButNoSubsettingAndWithBitmapsOnly: case FontEmbeddingRight.EditableButWithBitmapsOnly: case FontEmbeddingRight.InstallableButNoSubsettingAndWithBitmapsOnly: case FontEmbeddingRight.InstallableButWithBitmapsOnly: case FontEmbeddingRight.PreviewAndPrintButNoSubsettingAndWithBitmapsOnly: case FontEmbeddingRight.PreviewAndPrintButWithBitmapsOnly: fsType = "Bitmaps Only"; break; } designer = gtf.DesignerNames[english]; designerURL = gtf.DesignerUrls[english]; license = gtf.LicenseDescriptions[english]; manufacturer = gtf.ManufacturerNames[english]; manufacturerURL = gtf.VendorUrls[english]; trademark = gtf.Trademarks[english]; } catch (Exception e) { // file is somehow corrupt or not really a font file? Just ignore it. Console.WriteLine("GlyphTypeface for \"{0}\" threw an exception: {1}", group.Normal, e); determinedSuitability = kInvalid; determinedSuitabilityNotes = $"GlyphTypeface exception: {e}"; return; } #endif variants = group.GetAvailableVariants().ToArray(); // Now for the hard part: setting DeterminedSuitability // Check out the license information. if (!String.IsNullOrEmpty(license)) { if (license.Contains("Open Font License") || license.Contains("OFL") || license.StartsWith("Licensed under the Apache License") || license.Contains("GNU GPL") || license.Contains("GNU General Public License") || license.Contains(" GPL ") || license.Contains(" GNU ") || (license.Contains("GNU license") && license.Contains("www.gnu.org")) || license.Contains("GNU LGPL") || license.Contains("GNU Lesser General Public License")) { determinedSuitability = kOK; if (license.Contains("Open Font License") || license.Contains("OFL")) { determinedSuitabilityNotes = "Open Font License"; } else if (license.StartsWith("Licensed under the Apache License")) { determinedSuitabilityNotes = "Apache License"; } else if (license.Contains("GNU LGPL") || license.Contains("GNU Lesser General Public License")) { determinedSuitabilityNotes = "GNU LGPL"; } else { determinedSuitabilityNotes = "GNU GPL"; } return; } if (license.Replace('\n', ' ').Contains("free of charge") && license.Contains("Bitstream")) { determinedSuitability = kOK; determinedSuitabilityNotes = "Bitstream free license"; return; } } if (licenseURL == "http://dejavu-fonts.org/wiki/License") { determinedSuitability = kOK; determinedSuitabilityNotes = "Bitstream free license"; return; } if (!String.IsNullOrEmpty(copyright)) { // some people put license information in the copyright string. if (copyright.Contains("Artistic License")) { determinedSuitability = kOK; determinedSuitabilityNotes = "Artistic License"; return; } if (copyright.Contains("GNU General Public License") || copyright.Contains(" GPL ")) { determinedSuitability = kOK; determinedSuitabilityNotes = "GNU GPL"; return; } if (copyright.Contains("SIL Open Font License")) { determinedSuitability = kOK; determinedSuitabilityNotes = "Open Font License"; return; } if (copyright.Contains("Ubuntu Font Licence")) // British spelling I assume... { determinedSuitability = kOK; determinedSuitabilityNotes = "Ubuntu Font Licence"; return; } } if (fsType == "Restricted License" || fsType == "Bitmaps Only") { determinedSuitability = kUnsuitable; determinedSuitabilityNotes = "unambiguous fsType value"; return; } if (manufacturer == "Microsoft Corporation" || manufacturer == "Microsoft Corp." || (license != null && license.Contains("Microsoft supplied font") && manufacturer != null && manufacturer.Contains("Monotype")) || (license == null && manufacturer == null && copyright != null && copyright.Contains("Microsoft Corporation") && trademark != null && trademark.Contains("is a trademark of Microsoft Corporation")) ) { // Review what about "Print and Preview"? determinedSuitability = kOK; determinedSuitabilityNotes = "fsType from reliable source"; return; } // Give up. More heuristics may suggest themselves. determinedSuitability = kUnknown; determinedSuitabilityNotes = "no reliable information"; }
/// <summary> /// Note: There is some performance overhead to initializing this. /// </summary> private void InitializeFontData() { FontNameToFiles = new Dictionary <string, FontGroup>(); FontsWeCantInstall = new HashSet <string>(); #if __MonoCS__ using (var lib = new SharpFont.Library()) { // Find all the font files in the standard system location (/usr/share/font) and $HOME/.local/share/fonts (if it exists) foreach (var fontFile in FindLinuxFonts()) { try { using (var face = new SharpFont.Face(lib, fontFile)) { var embeddingTypes = face.GetFSTypeFlags(); if ((embeddingTypes & EmbeddingTypes.RestrictedLicense) == EmbeddingTypes.RestrictedLicense || (embeddingTypes & EmbeddingTypes.BitmapOnly) == EmbeddingTypes.BitmapOnly) { // Our font UI allows any font on the computer, but gives the user indications that some are more // useable in publishing Bloom books. The NoteFontsWeCantInstall prop is only true when we call this // from BloomPubMaker so that it can note that certain fonts are unsuitable for embedding in ePUBs. if (NoteFontsWeCantInstall) { FontsWeCantInstall.Add(face.FamilyName); continue; } } var name = face.FamilyName; // If you care about bold, italic, etc, you can filter here. FontGroup files; if (!FontNameToFiles.TryGetValue(name, out files)) { files = new FontGroup(); FontNameToFiles[name] = files; } files.Add(face, fontFile); } } catch (Exception) { continue; } } } #else foreach (var fontFile in FindWindowsFonts()) { GlyphTypeface gtf; try { gtf = new GlyphTypeface(new Uri("file:///" + fontFile)); } catch (Exception) { continue; // file is somehow corrupt or not really a font file? Just ignore it. } // Our font UI allows any font on the computer, but gives the user indications that some are more // useable in publishing Bloom books. The NoteFontsWeCantInstall prop is only true when we call this // from BloomPubMaker so that it can note that certain fonts are unsuitable for embedding in ePUBs. if (!FontIsEmbeddable(gtf.EmbeddingRights) && NoteFontsWeCantInstall) { string name1 = GetFontNameFromFile(fontFile); if (name1 != null) { FontsWeCantInstall.Add(name1); } continue; // not allowed to embed in ePUB } string name = GetFontNameFromFile(fontFile); if (name == null) { continue; // not sure how this can happen but I've seen it. } // If you care about bold, italic, etc, you can filter here. FontGroup files; if (!FontNameToFiles.TryGetValue(name, out files)) { files = new FontGroup(); FontNameToFiles[name] = files; } files.Add(gtf, fontFile); } #endif }