Exemplo n.º 1
0
        public static InternalSpreadsheet ReadFromFile(string path, IWebSocketProgress progress = null)
        {
            progress = progress ?? new NullWebSocketProgress();
            var result = new InternalSpreadsheet(false);

            try
            {
                SpreadsheetIO.ReadSpreadsheet(result, path);
            }
            catch (InvalidDataException e)
            {
                Bloom.Utils.MiscUtils.SuppressUnusedExceptionVarWarning(e);
                progress.MessageWithoutLocalizing(
                    "The input does not appear to be a valid Excel spreadsheet. Import failed.", ProgressKind.Error);
                return(null);
            }
            catch (SpreadsheetException se)
            {
                progress.MessageWithoutLocalizing(se.Message, ProgressKind.Error);
                return(null);
            }
            catch (Exception e)
            {
                progress.MessageWithoutLocalizing("Something went wrong reading the input file. Import failed. " + e.Message, ProgressKind.Error);
                return(null);
            }

            return(result);
        }
Exemplo n.º 2
0
        // Create a BloomReader book while also creating the temporary folder for it (according to the specified parameter) and disposing of it
        public static void CreateBloomPub(string outputPath, string bookFolderPath, BookServer bookServer,

                                          IWebSocketProgress progress, bool isTemplateBook, string tempFolderName = BRExportFolder,
                                          string creator = kCreatorBloom, AndroidPublishSettings settings = null)
        {
            using (var temp = new TemporaryFolder(tempFolderName))
            {
                CreateBloomPub(outputPath, bookFolderPath, bookServer, progress, temp, creator, isTemplateBook, settings);
            }
        }
Exemplo n.º 3
0
        /// <summary>
        /// Create a Bloom Digital book (the zipped .bloompub file) as used by BloomReader (and Bloom Library etc)
        /// </summary>
        /// <param name="outputPath">The path to create the zipped .bloompub output file at</param>
        /// <param name="bookFolderPath">The path to the input book</param>
        /// <param name="bookServer"></param>
        /// <param name="backColor"></param>
        /// <param name="progress"></param>
        /// <param name="tempFolder">A temporary folder. This function will not dispose of it when done</param>
        /// <param name="creator">value for &lt;meta name="creator" content="..."/&gt; (defaults to "bloom")</param>
        /// <param name="isTemplateBook"></param>
        /// <param name="settings"></param>
        /// <returns>Path to the unzipped .bloompub</returns>
        public static string CreateBloomPub(string outputPath, string bookFolderPath, BookServer bookServer,
                                            IWebSocketProgress progress, TemporaryFolder tempFolder, string creator = kCreatorBloom, bool isTemplateBook = false,
                                            AndroidPublishSettings settings = null)
        {
            var modifiedBook = PrepareBookForBloomReader(bookFolderPath, bookServer, tempFolder, progress, isTemplateBook, creator, settings);

            // We want at least 256 for Bloom Reader, because the screens have a high pixel density. And (at the moment) we are asking for
            // 64dp in Bloom Reader.

            BookCompressor.MakeSizedThumbnail(modifiedBook, modifiedBook.FolderPath, 256);

            BookCompressor.CompressBookDirectory(outputPath, modifiedBook.FolderPath, "", reduceImages: true, omitMetaJson: false, wrapWithFolder: false,
                                                 pathToFileForSha: BookStorage.FindBookHtmlInFolder(bookFolderPath));

            return(modifiedBook.FolderPath);
        }
Exemplo n.º 4
0
        /// <summary>
        /// Given a book, typically one in a temporary folder made just for exporting (or testing),
        /// and given the set of fonts found while creating that book and removing hidden elements,
        /// find the files needed for those fonts.
        /// 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="fontFileFinder">use new FontFinder() for real, or a stub in testing</param>
        public static void EmbedFonts(Book.Book book, IWebSocketProgress progress, HashSet <string> fontsWanted, IFontFinder fontFileFinder)
        {
            const string defaultFont = "Andika New Basic";             // already in BR, don't need to embed or make rule.

            fontsWanted.Remove(defaultFont);
            PublishHelper.CheckFontsForEmbedding(progress, fontsWanted, fontFileFinder, out List <string> filesToEmbed, out HashSet <string> badFonts);
            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)
            {
                if (badFonts.Contains(font))
                {
                    continue;
                }
                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");
            // Repair defaultLangStyles.css and other places in the output book if needed.
            if (badFonts.Any())
            {
                PublishHelper.FixCssReferencesForBadFonts(book.FolderPath, defaultFont, badFonts);
                PublishHelper.FixXmlDomReferencesForBadFonts(book.OurHtmlDom.RawDom, defaultFont, badFonts);
            }
        }
Exemplo n.º 5
0
        public static Book PrepareBookForBloomReader(Book book, BookServer bookServer, TemporaryFolder temp, Color backColor,
                                                     IWebSocketProgress progress)
        {
            var modifiedBook = BookCompressor.MakeDeviceXmatterTempBook(book, bookServer, temp.FolderPath);

            var jsonPath      = Path.Combine(temp.FolderPath, QuestionFileName);
            var questionPages = modifiedBook.RawDom.SafeSelectNodes(
                "//html/body/div[contains(@class, 'bloom-page') and contains(@class, 'questions')]");
            var questions = new List <QuestionGroup>();

            foreach (var page in questionPages.Cast <XmlElement>().ToArray())
            {
                MakeQuestion(page, questions);
                page.ParentNode.RemoveChild(page);
            }
            var builder = new StringBuilder("[");

            foreach (var question in questions)
            {
                if (builder.Length > 1)
                {
                    builder.Append(",\n");
                }
                builder.Append(question.GetJson());
            }
            builder.Append("]");
            File.WriteAllText(jsonPath, builder.ToString());

            // Do this after making questions, as they satisfy the criteria for being 'blank'
            modifiedBook.RemoveBlankPages();

            modifiedBook.SetAnimationDurationsFromAudioDurations();

            modifiedBook.OurHtmlDom.SetMedia("bloomReader");
            EmbedFonts(modifiedBook, progress, new FontFileFinder());

            modifiedBook.Save();

            return(modifiedBook);
        }
Exemplo n.º 6
0
        public bool Validate(InternalSpreadsheet sheet, IWebSocketProgress progress)
        {
            // An export would have several others. But none of them is absolutely required except this one.
            // (We could do without it, too, by assuming the first column contains them. But it's helpful to be
            // able to recognize spreadsheets created without any knowledge at all of the expected content.)
            // Note: depending on row content, this problem may be detected earlier in SpreadsheetIO while
            // converting the file to an InternalSpreadsheet.
            var rowTypeColumn = sheet.GetColumnForTag(InternalSpreadsheet.RowTypeColumnLabel);

            if (rowTypeColumn < 0)
            {
                progress.MessageWithoutLocalizing(MissingHeaderMessage, ProgressKind.Error);
                return(false);
            }
            var inputRows = sheet.ContentRows.ToList();

            if (!inputRows.Any(r => r.GetCell(rowTypeColumn).Content.StartsWith("[")))
            {
                progress.MessageWithoutLocalizing("This spreadsheet has no data that Bloom knows how to import. Did you follow the standard format for Bloom spreadsheets?", ProgressKind.Warning);
                // Technically this isn't a fatal error. We could just let the main loop do nothing. But reporting it as one avoids creating a spurious backup.
                return(false);
            }
            return(true);
        }
Exemplo n.º 7
0
        public static void CompressBookForDevice(string outputPath, Book book, BookServer bookServer, Color backColor, IWebSocketProgress progress)
        {
            using (var temp = new TemporaryFolder())
            {
                var modifiedBook = BloomReaderFileMaker.PrepareBookForBloomReader(book, bookServer, temp, backColor, progress);
                // We want at least 256 for Bloom Reader, because the screens have a high pixel density. And (at the moment) we are asking for
                // 64dp in Bloom Reader.

                MakeSizedThumbnail(modifiedBook, backColor, temp.FolderPath, 256);

                CompressDirectory(outputPath, modifiedBook.FolderPath, "", reduceImages: true, omitMetaJson: false, wrapWithFolder: false,
                                  pathToFileForSha: BookStorage.FindBookHtmlInFolder(book.FolderPath));
            }
        }
Exemplo n.º 8
0
        /// <summary>
        /// Import the spreadsheet into the dom
        /// </summary>
        /// <returns>a list of warnings</returns>
        public List <string> Import(InternalSpreadsheet sheet, IWebSocketProgress progress = null)
        {
            _sheet    = sheet;
            _progress = progress ?? new NullWebSocketProgress();
            Progress("Importing spreadsheet...");
            _warnings              = new List <string>();
            _inputRows             = _sheet.ContentRows.ToList();
            _pages                 = _destinationDom.GetPageElements().ToList();
            _bookIsLandscape       = _pages[0]?.Attributes["class"]?.Value?.Contains("Landscape") ?? false;
            _currentRowIndex       = 0;
            _currentPageIndex      = -1;
            _groupsOnPage          = new List <XmlElement>();
            _imageContainersOnPage = new List <XmlElement>();
            _destLayout            = Layout.FromDom(_destinationDom, Layout.A5Portrait);
            while (_currentRowIndex < _inputRows.Count)
            {
                var    currentRow   = _inputRows[_currentRowIndex];
                string rowTypeLabel = currentRow.MetadataKey;

                if (rowTypeLabel == InternalSpreadsheet.PageContentRowLabel)
                {
                    bool rowHasImage = !string.IsNullOrWhiteSpace(currentRow.GetCell(InternalSpreadsheet.ImageSourceColumnLabel).Text);
                    bool rowHasText  = RowHasText(currentRow);
                    if (rowHasImage && rowHasText)
                    {
                        AdvanceToNextGroupAndImageContainer();
                    }
                    else if (rowHasImage)
                    {
                        AdvanceToNextImageContainer();
                    }
                    else if (rowHasText)
                    {
                        AdvanceToNextGroup();
                    }
                    if (rowHasImage)
                    {
                        PutRowInImage(currentRow);
                    }
                    if (rowHasText)
                    {
                        PutRowInGroup(currentRow, _currentGroup);
                    }
                }
                else if (rowTypeLabel.StartsWith("[") && rowTypeLabel.EndsWith("]"))                 //This row is xmatter
                {
                    string dataBookLabel = rowTypeLabel.Substring(1, rowTypeLabel.Length - 2);       //remove brackets
                    UpdateDataDivFromRow(currentRow, dataBookLabel);
                }
                _currentRowIndex++;
            }
            // This section is necessary to make sure changes to the dom are recorded.
            // If we run SS Importer from the CLI (without CollectionSettings), BringBookUpToDate()
            // will happen when we eventually open the book, but the user gets an updated thumbail and preview
            // if we do it here for the main production case where we DO have both the CollectionSettings
            // and the Book itself. Testing is the other situation (mostly) that doesn't use CollectionSettings.
            if (_collectionSettings != null && _book != null)
            {
                _book.BringBookUpToDate(new NullProgress());
            }

            Progress("Done");
            return(_warnings);
        }
Exemplo n.º 9
0
 public void WriteToFile(string path, IWebSocketProgress progress = null)
 {
     SpreadsheetIO.WriteSpreadsheet(this, path, Params.RetainMarkup, progress);
 }
Exemplo n.º 10
0
        public static void WriteSpreadsheet(InternalSpreadsheet spreadsheet, string outputPath, bool retainMarkup, IWebSocketProgress progress = null)
        {
            using (var package = new ExcelPackage())
            {
                var worksheet = package.Workbook.Worksheets.Add("BloomBook");

                worksheet.DefaultColWidth = languageColumnWidth;
                for (int i = 1; i <= spreadsheet.StandardLeadingColumns.Length; i++)
                {
                    worksheet.Column(i).Width = standardLeadingColumnWidth;
                }

                var imageSourceColumn    = spreadsheet.GetColumnForTag(InternalSpreadsheet.ImageSourceColumnLabel);
                var imageThumbnailColumn = spreadsheet.GetColumnForTag(InternalSpreadsheet.ImageThumbnailColumnLabel);
                // Apparently the width is in some approximation of 'characters'. This empirically determined
                // conversion factor seems to do a pretty good job.
                worksheet.Column(imageThumbnailColumn + 1).Width = defaultImageWidth / 6.88;

                int r = 0;
                foreach (var row in spreadsheet.AllRows())
                {
                    r++;
                    for (var c = 0; c < row.Count; c++)
                    {
                        // Enhance: Excel complains about cells that contain pure numbers
                        // but are created as strings. We could possibly tell it that cells
                        // that contain simple numbers can be treated accordingly.
                        // It might be helpful for some uses of the group-on-page-index
                        // if Excel knew to treat them as numbers.

                        var sourceCell = row.GetCell(c);
                        var content    = sourceCell.Content;
                        // Parse xml for markdown formatting on language columns,
                        // Display formatting in excel spreadsheet
                        ExcelRange currentCell = worksheet.Cells[r, c + 1];
                        if (!string.IsNullOrEmpty(sourceCell.Comment))
                        {
                            // Second arg is supposed to be the author.
                            currentCell.AddComment(sourceCell.Comment, "Bloom");
                        }

                        if (!retainMarkup &&
                            IsWysiwygFormattedColumn(row, c) &&
                            IsWysiwygFormattedRow(row))
                        {
                            MarkedUpText markedUpText = MarkedUpText.ParseXml(content);
                            if (markedUpText.HasFormatting)
                            {
                                currentCell.IsRichText = true;
                                foreach (MarkedUpTextRun run in markedUpText.Runs)
                                {
                                    if (!run.Text.Equals(""))
                                    {
                                        ExcelRichText text = currentCell.RichText.Add(run.Text);
                                        text.Bold      = run.Bold;
                                        text.Italic    = run.Italic;
                                        text.UnderLine = run.Underlined;
                                        if (run.Superscript)
                                        {
                                            text.VerticalAlign = ExcelVerticalAlignmentFont.Superscript;
                                        }
                                    }
                                }
                            }
                            else
                            {
                                currentCell.Value = markedUpText.PlainText();
                            }
                        }
                        else
                        {
                            // Either the retainMarkup flag is set, or this is not book text. It could be header or leading column.
                            // Generally, we just want to blast our cell content into the spreadsheet cell.
                            // However, there are cases where we put an error message in an image thumbnail cell when processing the image path.
                            // We don't want to overwrite these. An easy way to prevent it is to not overwrite any cell that already has content.
                            // Since export is creating a new spreadsheet, cells we want to write will always be empty initially.
                            if (currentCell.Value == null)
                            {
                                currentCell.Value = content;
                            }
                        }


                        //Embed any images in the excel file
                        if (c == imageSourceColumn)
                        {
                            var imageSrc = sourceCell.Content;

                            // if this row has an image source value that is not a header
                            if (imageSrc != "" && !row.IsHeader)
                            {
                                var sheetFolder = Path.GetDirectoryName(outputPath);
                                var imagePath   = Path.Combine(sheetFolder, imageSrc);
                                //Images show up in the cell 1 row greater and 1 column greater than assigned
                                //So this will put them in row r, column imageThumbnailColumn+1 like we want
                                var rowHeight = embedImage(imagePath, r - 1, imageThumbnailColumn);
                                worksheet.Row(r).Height = rowHeight * 72 / 96 + 3;                               //so the image is visible; height seems to be points
                            }
                        }
                    }

                    if (row is HeaderRow)
                    {
                        using (ExcelRange rng = GetRangeForRow(worksheet, r))
                            rng.Style.Font.Bold = true;
                    }

                    if (row.Hidden)
                    {
                        worksheet.Row(r).Hidden = true;
                        SetBackgroundColorOfRow(worksheet, r, InternalSpreadsheet.HiddenColor);
                    }
                    else if (row.BackgroundColor != default(Color))
                    {
                        SetBackgroundColorOfRow(worksheet, r, row.BackgroundColor);
                    }
                }
                worksheet.Cells[1, 1, r, spreadsheet.ColumnCount].Style.WrapText = true;


                int embedImage(string imageSrcPath, int rowNum, int colNum)
                {
                    int finalHeight = 30;                     //  a reasonable default if we don't manage to embed an image.

                    try
                    {
                        using (Image image = Image.FromFile(imageSrcPath))
                        {
                            string imageName       = Path.GetFileNameWithoutExtension(imageSrcPath);
                            var    origImageHeight = image.Size.Height;
                            var    origImageWidth  = image.Size.Width;
                            int    finalWidth      = defaultImageWidth;
                            finalHeight = (int)(finalWidth * origImageHeight / origImageWidth);
                            var size = new Size(finalWidth, finalHeight);
                            using (Image thumbnail = ImageUtils.ResizeImageIfNecessary(size, image, false))
                            {
                                var excelImage = worksheet.Drawings.AddPicture(imageName, thumbnail);
                                excelImage.SetPosition(rowNum, 2, colNum, 2);
                            }
                        }
                    }
                    catch (Exception)
                    {
                        string errorText;
                        if (!RobustFile.Exists(imageSrcPath))
                        {
                            errorText = "Missing";
                        }
                        else if (Path.GetExtension(imageSrcPath).ToLowerInvariant().Equals(".svg"))
                        {
                            errorText = "Can't display SVG";
                        }
                        else
                        {
                            errorText = "Bad image file";
                        }
                        progress?.MessageWithoutLocalizing(errorText + ": " + imageSrcPath);
                        worksheet.Cells[r, imageThumbnailColumn + 1].Value = errorText;
                    }
                    return(Math.Max(finalHeight, 30));
                }

                foreach (var iColumn in spreadsheet.HiddenColumns)
                {
                    // This is pretty yucky... our internal spreadsheet is all 0-based, but the EPPlus library is all 1-based...
                    var iColumn1Based = iColumn + 1;

                    worksheet.Column(iColumn1Based).Hidden = true;
                    SetBackgroundColorOfColumn(worksheet, iColumn1Based, InternalSpreadsheet.HiddenColor);
                }

                try
                {
                    RobustFile.Delete(outputPath);
                    var xlFile = new FileInfo(outputPath);
                    package.SaveAs(xlFile);
                }
                catch (IOException ex) when((ex.HResult & 0x0000FFFF) == 32)                 //ERROR_SHARING_VIOLATION
                {
                    Console.WriteLine("Writing Spreadsheet failed. Do you have it open in another program?");
                    Console.WriteLine(ex.Message);
                    Console.WriteLine(ex.StackTrace);

                    progress?.Message("Spreadsheet.SpreadsheetLocked", "",
                                      "Bloom could not write to the spreadsheet because another program has it locked. Do you have it open in another program?",
                                      ProgressKind.Error);
                }
                catch (Exception ex)
                {
                    progress?.Message("Spreadsheet.ExportFailed", "",
                                      "Export failed: " + ex.Message,
                                      ProgressKind.Error);
                }
            }
        }
Exemplo n.º 11
0
        /// <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");
        }
Exemplo n.º 12
0
        /// <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
            }
        }
Exemplo n.º 13
0
 public static void SendBatchedWarningMessagesToProgress(ISet <string> warningMessages, IWebSocketProgress progress)
 {
     if (warningMessages.Any())
     {
         progress.Message("Common.Warning", "Warning", ProgressKind.Warning, false);
     }
     foreach (var warningMessage in warningMessages)
     {
         // Messages are already localized
         progress.MessageWithoutLocalizing(warningMessage, ProgressKind.Warning);
     }
 }
Exemplo n.º 14
0
        public static Book.Book PrepareBookForBloomReader(string bookFolderPath, BookServer bookServer,
                                                          TemporaryFolder temp,
                                                          IWebSocketProgress progress, bool isTemplateBook,
                                                          string creator = kCreatorBloom,
                                                          AndroidPublishSettings settings = null)
        {
            // MakeDeviceXmatterTempBook needs to be able to copy customCollectionStyles.css etc into parent of bookFolderPath
            // And bloom-player expects folder name to match html file name.
            var htmPath = BookStorage.FindBookHtmlInFolder(bookFolderPath);
            var tentativeBookFolderPath = Path.Combine(temp.FolderPath,
                                                       // Windows directory names cannot have trailing periods, but FileNameWithoutExtension can have these.  (BH-6097)
                                                       BookStorage.SanitizeNameForFileSystem(Path.GetFileNameWithoutExtension(htmPath)));

            Directory.CreateDirectory(tentativeBookFolderPath);
            var modifiedBook = PublishHelper.MakeDeviceXmatterTempBook(bookFolderPath, bookServer,
                                                                       tentativeBookFolderPath, isTemplateBook);

            modifiedBook.SetMotionAttributesOnBody(settings?.Motion ?? false);

            // Although usually tentativeBookFolderPath and modifiedBook.FolderPath are the same, there are some exceptions
            // In the process of bringing a book up-to-date (called by MakeDeviceXmatterTempBook), the folder path may change.
            // For example, it could change if the original folder path contains punctuation marks now deemed dangerous.
            //    The book will be moved to the sanitized version of the file name instead.
            // It can also happen if we end up picking a different version of the title (i.e. in a different language)
            //    than the one written to the .htm file.
            string modifiedBookFolderPath = modifiedBook.FolderPath;

            if (modifiedBook.CollectionSettings.HaveEnterpriseFeatures)
            {
                ProcessQuizzes(modifiedBookFolderPath, modifiedBook.RawDom);
            }

            // Right here, let's maintain the history of what the BloomdVersion signifies to a reader.
            // Version 1 (as opposed to no BloomdVersion field): the bookFeatures property may be
            // used to report features analytics (with earlier bloompub's, the reader must use its own logic)
            modifiedBook.Storage.BookInfo.MetaData.BloomdVersion = 1;

            modifiedBook.Storage.BookInfo.UpdateOneSingletonTag("distribution", settings?.DistributionTag);
            if (!string.IsNullOrEmpty(settings?.BookshelfTag))
            {
                modifiedBook.Storage.BookInfo.UpdateOneSingletonTag("bookshelf", settings.BookshelfTag);
            }

            if (settings?.RemoveInteractivePages ?? false)
            {
                var activities = modifiedBook.GetPageElements().Cast <XmlNode>()
                                 .Where(x => x is XmlElement elt && HtmlDom.IsActivityPage(elt)).ToArray();
                foreach (var page in activities)
                {
                    page.ParentNode.RemoveChild(page);
                }
            }

            if (settings?.LanguagesToInclude != null)
            {
                PublishModel.RemoveUnwantedLanguageData(modifiedBook.OurHtmlDom, settings.LanguagesToInclude, modifiedBook.BookData.MetadataLanguage1IsoCode);
                PublishModel.RemoveUnwantedLanguageRulesFromCssFiles(modifiedBook.FolderPath, settings.LanguagesToInclude);
            }
            else if (Program.RunningHarvesterMode && modifiedBook.OurHtmlDom.SelectSingleNode(BookStorage.ComicalXpath) != null)
            {
                // This indicates that we are harvesting a book with comic speech bubbles or other overlays (Overlay Tool).
                // For books with overlays, we only publish a single language. It's not currently feasible to
                // allow the reader to switch language in a book with overlays, because typically that requires
                // adjusting the positions of the overlays, and we don't yet support having more than one
                // set of overlay locations in a single book. See BL-7912 for some ideas on how we might
                // eventually improve this. In the meantime, switching language would have bad effects,
                // and if you can't switch language, there's no point in the book containing more than one.
                var languagesToInclude = new string[1] {
                    modifiedBook.BookData.Language1.Iso639Code
                };
                PublishModel.RemoveUnwantedLanguageData(modifiedBook.OurHtmlDom, languagesToInclude, modifiedBook.BookData.MetadataLanguage1IsoCode);
            }

            // Do this after processing interactive pages, as they can satisfy the criteria for being 'blank'
            HashSet <string> fontsUsed = null;

            using (var helper = new PublishHelper())
            {
                helper.ControlForInvoke = ControlForInvoke;
                ISet <string> warningMessages = new HashSet <string>();
                helper.RemoveUnwantedContent(modifiedBook.OurHtmlDom, modifiedBook, false,
                                             warningMessages, keepPageLabels: settings?.WantPageLabels ?? false);
                PublishHelper.SendBatchedWarningMessagesToProgress(warningMessages, progress);
                fontsUsed = helper.FontsUsed;
            }
            if (!modifiedBook.IsTemplateBook)
            {
                modifiedBook.RemoveBlankPages(settings?.LanguagesToInclude);
            }

            // See https://issues.bloomlibrary.org/youtrack/issue/BL-6835.
            RemoveInvisibleImageElements(modifiedBook);
            modifiedBook.Storage.CleanupUnusedSupportFiles(/*isForPublish:*/ true, settings?.AudioLanguagesToExclude);
            if (!modifiedBook.IsTemplateBook && RobustFile.Exists(Path.Combine(modifiedBookFolderPath, "placeHolder.png")))
            {
                RobustFile.Delete(Path.Combine(modifiedBookFolderPath, "placeHolder.png"));
            }
            modifiedBook.RemoveObsoleteAudioMarkup();

            // We want these to run after RemoveUnwantedContent() so that the metadata will more accurately reflect
            // the subset of contents that are included in the .bloompub
            // Note that we generally want to disable features here, but not enable them, especially while
            // running harvester!  See https://issues.bloomlibrary.org/youtrack/issue/BL-8995.
            var enableBlind = modifiedBook.BookInfo.MetaData.Feature_Blind || !Program.RunningHarvesterMode;
            // BloomReader and BloomPlayer are not using the SignLanguage feature, and it's misleading to
            // assume the existence of videos implies sign language.  There is a separate "Video" feature
            // now that gets set automatically.  (Automated setting of the Blind feature is imperfect, but
            // more meaningful than trying to automate sign language just based on one video existing.)
            var enableSignLanguage = modifiedBook.BookInfo.MetaData.Feature_SignLanguage;

            modifiedBook.UpdateMetadataFeatures(
                isBlindEnabled: enableBlind,
                isSignLanguageEnabled: enableSignLanguage,
                isTalkingBookEnabled: true,            // talkingBook is only ever set automatically as far as I can tell.
                allowedLanguages: null                 // allow all because we've already filtered out the unwanted ones from the dom above.
                );

            modifiedBook.SetAnimationDurationsFromAudioDurations();

            modifiedBook.OurHtmlDom.SetMedia("bloomReader");
            modifiedBook.OurHtmlDom.AddOrReplaceMetaElement("bloom-digital-creator", creator);
            EmbedFonts(modifiedBook, progress, fontsUsed, FontFileFinder.GetInstance(Program.RunningUnitTests));

            var bookFile = BookStorage.FindBookHtmlInFolder(modifiedBook.FolderPath);

            StripImgIfWeCannotFindFile(modifiedBook.RawDom, bookFile);
            StripContentEditableAndTabIndex(modifiedBook.RawDom);
            InsertReaderStylesheet(modifiedBook.RawDom);
            RobustFile.Copy(FileLocationUtilities.GetFileDistributedWithApplication(BloomFileLocator.BrowserRoot, "publish", "ReaderPublish", "readerStyles.css"),
                            Path.Combine(modifiedBookFolderPath, "readerStyles.css"));
            ConvertImagesToBackground(modifiedBook.RawDom);

            AddDistributionFile(modifiedBookFolderPath, creator, settings);

            modifiedBook.Save();

            return(modifiedBook);
        }
Exemplo n.º 15
0
 public static void CreateBloomPub(string outputPath, Book.Book book, BookServer bookServer, IWebSocketProgress progress, AndroidPublishSettings settings = null)
 {
     CreateBloomPub(outputPath, book.FolderPath, bookServer, progress, book.IsTemplateBook, settings: settings);
 }
Exemplo n.º 16
0
        public static void CreateBloomPub(BookInfo bookInfo, AndroidPublishSettings settings, string outputFolder, BookServer bookServer, IWebSocketProgress progress)
        {
            var outputPath = Path.Combine(outputFolder, Path.GetFileName(bookInfo.FolderPath) + BookCompressor.BloomPubExtensionWithDot);

            BloomPubMaker.CreateBloomPub(outputPath, bookInfo.FolderPath, bookServer, progress, bookInfo.IsSuitableForMakingShells, settings: settings);
        }