/// <summary> /// Given a book, typically one in a temporary folder made just for exporting (or testing), /// examine the CSS files and determine what fonts should be necessary. (Enhance: we could actually /// load the book into a DOM and find out what font IS used for each block.) /// Copy the font file for the normal style of that font family from the system font folder, /// if permitted; or post a warning in progress if we can't embed it. /// Create an extra css file (fonts.css) which tells the book to find the font files for those font families /// in the local folder, and insert a link to it into the book. /// </summary> /// <param name="book"></param> /// <param name="progress"></param> /// <param name="fontFileFinder">use new FontFinder() for real, or a stub in testing</param> public static void EmbedFonts(Book book, IWebSocketProgress progress, IFontFinder fontFileFinder) { const string defaultFont = "Andika New Basic"; // already in BR, don't need to embed or make rule. // The 'false' here says to ignore all but the first font face in CSS's ordered lists of desired font faces. // If someone is publishing an Epub, they should have that font showing. For one thing, this makes it easier // for us to not embed fonts we don't want/ need.For another, it makes it less likely that an epub will look // different or have glyph errors when shown on a machine that does have that primary font. var fontsWanted = EpubMaker.GetFontsUsed(book.FolderPath, false).ToList(); fontsWanted.Remove(defaultFont); fontFileFinder.NoteFontsWeCantInstall = true; var filesToEmbed = new List <string>(); foreach (var font in fontsWanted) { var fontFiles = fontFileFinder.GetFilesForFont(font); if (fontFiles.Count() > 0) { filesToEmbed.AddRange(fontFiles); progress.MessageWithParams("CheckFontOK", "{0} is a font name", "Checking {0} font: License OK for embedding.", font); // Assumes only one font file per font; if we embed multiple ones will need to enhance this. var size = new FileInfo(fontFiles.First()).Length; var sizeToReport = (size / 1000000.0).ToString("F1"); // purposely locale-specific; might be e.g. 1,2 progress.MessageWithColorAndParams("Embedding", "{1} is a number with one decimal place, the number of megabytes the font file takes up", "blue", "Embedding font {0} at a cost of {1} megs", font, sizeToReport); continue; } if (fontFileFinder.FontsWeCantInstall.Contains(font)) { progress.ErrorWithParams("LicenseForbids", "{0} is a font name", "Checking {0} font: License does not permit embedding.", font); } else { progress.ErrorWithParams("NoFontFound", "{0} is a font name", "Checking {0} font: No font found to embed.", font); } progress.ErrorWithParams("SubstitutingAndika", "{0} and {1} are font names", "Substituting \"{0}\" for \"{1}\"", defaultFont, font); } foreach (var file in filesToEmbed) { // Enhance: do we need to worry about problem characters in font file names? var dest = Path.Combine(book.FolderPath, Path.GetFileName(file)); RobustFile.Copy(file, dest); } // Create the fonts.css file, which tells the browser where to find the fonts for those families. var sb = new StringBuilder(); foreach (var font in fontsWanted) { var group = fontFileFinder.GetGroupForFont(font); if (group != null) { EpubMaker.AddFontFace(sb, font, "normal", "normal", group.Normal); } // We don't need (or want) a rule to use Andika instead. // The reader typically WILL use Andika, because we have a rule making it the default font // for the whole body of the document, and BloomReader always has it available. // However, it's possible that although we aren't allowed to embed the desired font, // the device actually has it installed. In that case, we want to use it. } RobustFile.WriteAllText(Path.Combine(book.FolderPath, "fonts.css"), sb.ToString()); // Tell the document to use the new stylesheet. book.OurHtmlDom.AddStyleSheet("fonts.css"); }
/// <summary> /// Checks the wanted fonts for being valid for embedding, both for licensing and for the type of file /// (based on the filename extension). /// The list of rejected fonts is returned in badFonts and the list of files to copy for good fonts is /// returned in filesToEmbed. Messages are written to the progress output as the processing goes along. /// </summary> /// <remarks> /// fontFileFinder must be either a new instance or a stub for testing. /// Setting fontFileFinder.NoteFontsWeCantInstall ensures that fontFileFinder.GetFilesForFont(font) /// will not return any files for fonts that we know cannot be embedded without reference to the /// license details. /// </remarks> public static void CheckFontsForEmbedding(IWebSocketProgress progress, HashSet <string> fontsWanted, IFontFinder fontFileFinder, out List <string> filesToEmbed, out HashSet <string> badFonts) { filesToEmbed = new List <string>(); badFonts = new HashSet <string>(); const string defaultFont = "Andika New Basic"; fontFileFinder.NoteFontsWeCantInstall = true; if (_fontMetadataMap == null) { _fontMetadataMap = new Dictionary <string, FontMetadata>(); foreach (var meta in FontsApi.AvailableFontMetadata) { _fontMetadataMap.Add(meta.name, meta); } } foreach (var font in fontsWanted) { var fontFiles = fontFileFinder.GetFilesForFont(font); var filesFound = fontFiles.Any(); // unembeddable fonts determined don't have any files recorded var badLicense = false; var missingLicense = false; var badFileType = false; var fileExtension = ""; if (_fontMetadataMap.TryGetValue(font, out var meta)) { fileExtension = meta.fileExtension; switch (meta.determinedSuitability) { case FontMetadata.kUnsuitable: badLicense = true; missingLicense = false; break; case FontMetadata.kInvalid: // We don't really know the values for badLicense and missingLicense, but they don't matter. badFileType = true; break; case FontMetadata.kUnknown: // same as not finding the metadata for the font. badLicense = false; missingLicense = true; break; case FontMetadata.kOK: badLicense = false; missingLicense = false; break; } } else { missingLicense = true; // This is usually covered by the case kInvalid above, but needed if no metadata at all. if (filesFound) { fileExtension = Path.GetExtension(fontFiles.First()).ToLowerInvariant(); badFileType = !FontMetadata.fontFileTypesBloomKnows.Contains(fileExtension); } } if (filesFound && !badFileType && !badLicense) { filesToEmbed.AddRange(fontFiles); if (missingLicense) { progress.MessageWithParams("PublishTab.Android.File.Progress.UnknownLicense", "{0} is a font name", "Checking {0} font: Unknown license", ProgressKind.Progress, font); } else { progress.MessageWithParams("PublishTab.Android.File.Progress.CheckFontOK", "{0} is a font name", "Checking {0} font: License OK for embedding.", ProgressKind.Progress, font); } // Assumes only one font file per font; if we embed multiple ones will need to enhance this. var size = new FileInfo(fontFiles.First()).Length; var sizeToReport = (size / 1000000.0).ToString("F1"); // purposely locale-specific; might be e.g. 1,2 progress.MessageWithParams("PublishTab.Android.File.Progress.Embedding", "{1} is a number with one decimal place, the number of megabytes the font file takes up", "Embedding font {0} at a cost of {1} megs", ProgressKind.Note, font, sizeToReport); continue; } if (badFileType) { progress.MessageWithParams("PublishTab.Android.File.Progress.IncompatibleFontFileFormat", "{0} is a font name, {1} is a file extension (for example: .ttc)", "This book has text in a font named \"{0}\". Bloom cannot publish this font's format ({1}).", ProgressKind.Error, font, fileExtension); } else if (fontFileFinder.FontsWeCantInstall.Contains(font) || badLicense) { progress.MessageWithParams("PublishTab.Android.File.Progress.LicenseForbids", "{0} is a font name", "This book has text in a font named \"{0}\". The license for \"{0}\" does not permit Bloom to embed the font in the book.", ProgressKind.Error, font); } else { progress.MessageWithParams("PublishTab.Android.File.Progress.NoFontFound", "{0} is a font name", "This book has text in a font named \"{0}\", but Bloom could not find that font on this computer.", ProgressKind.Error, font); } progress.MessageWithParams("PublishTab.Android.File.Progress.SubstitutingAndika", "{0} is a font name", "Bloom will substitute \"{0}\" instead.", ProgressKind.Error, defaultFont, font); badFonts.Add(font); // need to prevent the bad/missing font from showing up in fonts.css and elsewhere } }