public static void SendBatchedWarningMessagesToProgress(ISet <string> warningMessages, WebSocketProgress 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); } }
public static void Save(Book.Book book, BookServer bookServer, Color backColor, WebSocketProgress progress) { var progressWithL10N = progress.WithL10NPrefix("PublishTab.Android.File.Progress."); using (var dlg = new DialogAdapters.SaveFileDialogAdapter()) { dlg.DefaultExt = BookCompressor.ExtensionForDeviceBloomBook; var bloomdFileDescription = LocalizationManager.GetString("PublishTab.Android.bloomdFileFormatLabel", "Bloom Book for Devices", "This is shown in the 'Save' dialog when you save a bloom book in the format that works with the Bloom Reader Android App"); dlg.Filter = $"{bloomdFileDescription}|*{BookCompressor.ExtensionForDeviceBloomBook}"; dlg.FileName = Path.GetFileName(book.FolderPath) + BookCompressor.ExtensionForDeviceBloomBook; if (!string.IsNullOrWhiteSpace(Settings.Default.BloomDeviceFileExportFolder) && Directory.Exists(Settings.Default.BloomDeviceFileExportFolder)) { dlg.InitialDirectory = Settings.Default.BloomDeviceFileExportFolder; //(otherwise leave to default save location) } if (DialogResult.OK == dlg.ShowDialog()) { Settings.Default.BloomDeviceFileExportFolder = Path.GetDirectoryName(dlg.FileName); AndroidView.CheckBookLayout(book, progress); PublishToAndroidApi.SendBook(book, bookServer, dlg.FileName, null, progressWithL10N, (publishedFileName, bookTitle) => progressWithL10N.GetMessageWithParams("Saving", "{0} is a file path", "Saving as {0}", dlg.FileName), null, backColor); PublishToAndroidApi.ReportAnalytics("file", book); } } }
public void UpdateAndSave(EpubPublishUiSettings newSettings, string path, bool force, WebSocketProgress progress = null) { bool succeeded; do { lock (this) { succeeded = UpdatePreview(newSettings, force, progress); if (succeeded) { EpubMaker.SaveEpub(path, _progress); _webSocketServer.SendString(kWebsocketContext, kWebsocketEventId_epubReady, _previewSrc); } } } while (!succeeded && !EpubMaker.AbortRequested); // try until we get a complete epub, not interrupted by user changing something. }
public bool UpdatePreview(EpubPublishUiSettings newSettings, bool force, WebSocketProgress progress = null) { _progress = progress ?? _standardProgress.WithL10NPrefix("PublishTab.Epub."); if (Program.RunningOnUiThread) { // There's some stuff inside this lock that has to run on the UI thread. // If we lock the UI thread here, we can deadlock the whole program. throw new ApplicationException(@"Must not attempt to make epubs on UI thread...will produce deadlocks"); } lock (_epubMakerLock) { if (EpubMaker != null) { EpubMaker.AbortRequested = false; } _stagingEpub = true; } // For some unknown reason, if the accessibility window is showing, some of the browser navigation // that is needed to accurately determine which content is visible simply doesn't happen. // It would be disconcerting if it popped to the top after we close it and reopen it. // So, we just close the window if it is showing when we do this. See BL-7807. // Except that opening the Ace Checker tab invokes this code path in a way that works without the // deadlock (or whatever causes the failure). This call can be detected by the progress argument not // being null. The Refresh button on the AccessibilityCheckWindow also uses this code path in the // same way, so the next two lines also allow that Refresh button to work. See BL-9341 for why // the original fix is inadequate. if (progress == null) { AccessibilityChecker.AccessibilityCheckWindow.StaticClose(); } try { _webSocketServer.SendString(kWebsocketContext, "startingEbookCreation", _previewSrc); var htmlPath = _bookSelection.CurrentSelection.GetPathHtmlFile(); var newVersion = Book.Book.ComputeHashForAllBookRelatedFiles(htmlPath); bool previewIsAlreadyCurrent; lock (_epubMakerLock) { previewIsAlreadyCurrent = _desiredEpubSettings == newSettings && EpubMaker != null && newVersion == _bookVersion && !EpubMaker.AbortRequested && !force; } if (previewIsAlreadyCurrent) { SaveAsEpub(); // just in case there's a race condition where we haven't already saved it. return(true); // preview is already up to date. } _desiredEpubSettings = newSettings; // clear the obsolete preview, if any; this also ensures that when the new one gets done, // we will really be changing the src attr in the preview iframe so the display will update. _webSocketServer.SendEvent(kWebsocketContext, kWebsocketEventId_epubReady); _bookVersion = newVersion; ReportProgress(LocalizationManager.GetString("PublishTab.Epub.PreparingPreview", "Preparing Preview")); // This three-tries loop is an attempt to recover from a weird state the system sometimes gets into // where a browser won't navigate to a temporary page that the EpubMaker uses. I'm not sure it actually // helps, once the system gets into this state even a brand new browser seems to have the same problem. // Usually there will be no exception, and the loop breaks at the end of the first iteration. for (int i = 0; i < 3; i++) { try { if (!PublishHelper.InPublishTab) { return(false); } _previewSrc = UpdateEpubControlContent(); } catch (ApplicationException ex) { Bloom.Utils.MiscUtils.SuppressUnusedExceptionVarWarning(ex); if (i >= 2) { throw; } ReportProgress("Something went wrong, trying again"); continue; } break; // normal case, no exception } lock (_epubMakerLock) { if (EpubMaker.AbortRequested) { return(false); // the code that set the abort flag will request a new preview. } } } finally { lock (_epubMakerLock) { _stagingEpub = false; } } // Do pending save if the user requested it while the preview was still in progress. SaveAsEpub(); ReportProgress(LocalizationManager.GetString("PublishTab.Epub.Done", "Done")); return(true); }
public static Book.Book PrepareBookForBloomReader(string bookFolderPath, BookServer bookServer, TemporaryFolder temp, WebSocketProgress 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, Path.GetFileNameWithoutExtension(htmPath)); Directory.CreateDirectory(tentativeBookFolderPath); var modifiedBook = PublishHelper.MakeDeviceXmatterTempBook(bookFolderPath, bookServer, tentativeBookFolderPath, isTemplateBook); // 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 bloomd's, the reader must use its own logic) modifiedBook.Storage.BookInfo.MetaData.BloomdVersion = 1; if (settings?.LanguagesToInclude != null) { PublishModel.RemoveUnwantedLanguageData(modifiedBook.OurHtmlDom, settings.LanguagesToInclude, modifiedBook.BookData.MetadataLanguage1IsoCode); } else if (Program.RunningHarvesterMode && modifiedBook.OurHtmlDom.SelectSingleNode(BookStorage.ComicalXpath) != null) { // This indicates that we are harvesting a book with comic speech bubbles. // For comical books, we only publish a single language. It's not currently feasible to // allow the reader to switch language in a Comical book, because typically that requires // adjusting the positions of the bubbles, and we don't yet support having more than one // set of bubble 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); 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 .bloomd // 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. 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); modifiedBook.Save(); return(modifiedBook); }
internal WiFiAdvertiser(WebSocketProgress progress) { _progress = progress; }
public static void CreateBloomPub(string outputPath, Book.Book book, BookServer bookServer, Color backColor, WebSocketProgress progress, AndroidPublishSettings settings = null) { CreateBloomPub(outputPath, book.FolderPath, bookServer, backColor, progress, book.IsTemplateBook, settings: settings); }
/// <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="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 book, WebSocketProgress 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); 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("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 (fontFileFinder.FontsWeCantInstall.Contains(font)) { //progress.Error("Common.Warning", "Warning"); progress.MessageWithParams("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("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("SubstitutingAndika", "{0} is a font name", "Bloom will substitute \"{0}\" instead.", ProgressKind.Error, 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> /// This is the core of sending a book to a device. We need a book and a bookServer in order to come up /// with the .bloomd file. /// We are either simply saving the .bloomd to destFileName, or else we will make a temporary .bloomd file and /// actually send it using sendAction. /// We report important progress on the progress control. This includes reporting that we are starting /// the actual transmission using startingMessageAction, which is passed the safe file name (for checking pre-existence /// in UsbPublisher) and the book title (typically inserted into the message). /// If a confirmAction is passed (currently only by UsbPublisher), we use it check for a successful transfer /// before reporting completion (except for file save, where the current message is inappropriate). /// This is an awkward case where the three ways of publishing are similar enough that /// it's annoying and dangerous to have three entirely separate methods but somewhat awkward to combine them. /// Possibly we could eventually make them more similar, e.g., it would simplify things if they all said /// "Sending X to Y", though I'm not sure that would be good i18n if Y is sometimes a device name /// and sometimes a path. /// </summary> /// <param name="book"></param> /// <param name="destFileName"></param> /// <param name="sendAction"></param> /// <param name="progress"></param> /// <param name="bookServer"></param> /// <param name="startingMessageFunction"></param> public static void SendBook(Book.Book book, BookServer bookServer, string destFileName, Action <string, string> sendAction, WebSocketProgress progress, Func <string, string, string> startingMessageFunction, Func <string, bool> confirmFunction, Color backColor) { var bookTitle = book.Title; progress.MessageUsingTitle("PackagingBook", "Packaging \"{0}\" for use with Bloom Reader...", bookTitle); // compress audio if needed, with progress message if (AudioProcessor.IsAnyCompressedAudioMissing(book.FolderPath, book.RawDom)) { progress.Message("CompressingAudio", "Compressing audio files"); AudioProcessor.TryCompressingAudioAsNeeded(book.FolderPath, book.RawDom); } var publishedFileName = BookStorage.SanitizeNameForFileSystem(bookTitle) + BookCompressor.ExtensionForDeviceBloomBook; if (startingMessageFunction != null) { progress.MessageWithoutLocalizing(startingMessageFunction(publishedFileName, bookTitle)); } if (destFileName == null) { // wifi or usb...make the .bloomd in a temp folder. using (var bloomdTempFile = TempFile.WithFilenameInTempFolder(publishedFileName)) { BookCompressor.CompressBookForDevice(bloomdTempFile.Path, book, bookServer, backColor, progress); sendAction(publishedFileName, bloomdTempFile.Path); if (confirmFunction != null && !confirmFunction(publishedFileName)) { throw new ApplicationException("Book does not exist after write operation."); } progress.MessageUsingTitle("BookSent", "You can now read \"{0}\" in Bloom Reader!", bookTitle); } } else { // save file...user has supplied name, there is no further action. Debug.Assert(sendAction == null, "further actions are not supported when passing a path name"); BookCompressor.CompressBookForDevice(destFileName, book, bookServer, backColor, progress); } }
public WiFiPublisher(WebSocketProgress progress, BookServer bookServer) { _bookServer = bookServer; _progress = progress.WithL10NPrefix("PublishTab.Android.Wifi.Progress."); }
public static void DoWorkWithProgressDialog(IBloomWebSocketServer socketServer, string socketContext, Func <Form> makeDialog, Func <IWebSocketProgress, BackgroundWorker, bool> doWhat, Action <Form> doWhenMainActionFalse = null, IWin32Window owner = null) { var progress = new WebSocketProgress(socketServer, socketContext); // NOTE: This (specifically ShowDialog) blocks the main thread until the dialog is closed. // Be careful to avoid deadlocks. using (var dlg = makeDialog()) { // For now let's not try to handle letting the user abort. dlg.ControlBox = false; var worker = new BackgroundWorker(); worker.WorkerSupportsCancellation = true; worker.DoWork += (sender, args) => { ProgressDialogApi.SetCancelHandler(() => { worker.CancelAsync(); }); // A way of waiting until the dialog is ready to receive progress messages while (!socketServer.IsSocketOpen(socketContext)) { Thread.Sleep(50); } bool waitForUserToCloseDialogOrReportProblems; try { waitForUserToCloseDialogOrReportProblems = doWhat(progress, worker); } catch (Exception ex) { // depending on the nature of the problem, we might want to do more or less than this. // But at least this lets the dialog reach one of the states where it can be closed, // and gives the user some idea things are not right. socketServer.SendEvent(socketContext, "finished"); waitForUserToCloseDialogOrReportProblems = true; progress.MessageWithoutLocalizing("Something went wrong: " + ex.Message, ex is FatalException? ProgressKind.Fatal:ProgressKind.Error); } // stop the spinner socketServer.SendEvent(socketContext, "finished"); if (waitForUserToCloseDialogOrReportProblems) { // Now the user is allowed to close the dialog or report problems. // (ProgressDialog in JS-land is watching for this message, which causes it to turn // on the buttons that allow the dialog to be manually closed (or a problem to be reported). socketServer.SendBundle(socketContext, "show-buttons", new DynamicJson()); } else { // Just close the dialog dlg.Invoke((Action)(() => { if (doWhenMainActionFalse != null) { doWhenMainActionFalse(dlg); } else { dlg.Close(); } })); } }; worker.RunWorkerAsync(); dlg.ShowDialog(owner); // returns when dialog closed if (progress.HasFatalProblemBeenReported) { Application.Exit(); } ProgressDialogApi.SetCancelHandler(null); } }
public MockUsbPublisher(WebSocketProgress progress, BookServer bookServer) : base(progress, bookServer) { Stopped = () => SetState("dummy"); _exceptionToThrow = HR_ERROR_DISK_FULL; }
public UsbPublisher(WebSocketProgress progress, BookServer bookServer) { _bookServer = bookServer; _progress = progress.WithL10NPrefix("PublishTab.Android.Usb.Progress."); _androidDeviceUsbConnection = new AndroidDeviceUsbConnection(); }