private string GetPdfPath(string fname) { string path = null; // Sanitize fileName first string fileName = BookStorage.SanitizeNameForFileSystem(fname); for (int i = 0; i < 100; i++) { path = Path.Combine(Path.GetTempPath(), string.Format("{0}-{1}.pdf", fileName, i)); if (!RobustFile.Exists(path)) { break; } try { RobustFile.Delete(path); break; } catch (Exception) { //couldn't delete it? then increment the suffix and try again } } return(path); }
public void Start(Book.Book book, CollectionSettings collectionSettings, Color backColor) { if (_wifiAdvertiser != null) { Stop(); } // This listens for a BloomReader to request a book. // It requires a firewall hole allowing Bloom to receive messages on _portToListen. // We initialize it before starting the Advertiser to avoid any chance of a race condition // where a BloomReader manages to request an advertised book before we start the listener. _wifiListener = new BloomReaderUDPListener(); _wifiListener.NewMessageReceived += (sender, args) => { var json = Encoding.UTF8.GetString(args.Data); try { dynamic settings = JsonConvert.DeserializeObject(json); // The property names used here must match the ones in BloomReader, doInBackground method of SendMessage, // a private class of NewBookListenerService. var androidIpAddress = (string)settings.deviceAddress; var androidName = (string)settings.deviceName; // This prevents the device (or other devices) from queuing up requests while we're busy with this one. // In effect, the Android is only allowed to request a retry after we've given up this try at sending. // Of course, there are async effects from network latency. But if we do get another request while // handling this one, we will ignore it, since StartSendBook checks for a transfer in progress. _wifiAdvertiser.Paused = true; StartSendBookOverWiFi(book, androidIpAddress, androidName, backColor); // Returns immediately. But we don't resume advertisements until the async send completes. } // If there's something wrong with the JSON (maybe an obsolete or newer version of reader?) // just ignore the request. catch (Exception ex) when(ex is JsonReaderException || ex is JsonSerializationException) { _progress.Error(id: "BadBookRequest", message: "Got a book request we could not process. Possibly the device is running an incompatible version of BloomReader?"); //this is too technical/hard to translate _progress.ErrorWithoutLocalizing($" Request contains {json}; trying to interpret as JSON we got {ex.Message}"); } }; var pathHtmlFile = book.GetPathHtmlFile(); _wifiAdvertiser = new WiFiAdvertiser(_progress) { BookTitle = BookStorage.SanitizeNameForFileSystem(book.Title), // must be the exact same name as the file we will send if requested TitleLanguage = collectionSettings.Language1Iso639Code, BookVersion = Book.Book.MakeVersionCode(File.ReadAllText(pathHtmlFile), pathHtmlFile) }; AndroidView.CheckBookLayout(book, _progress); _wifiAdvertiser.Start(); _progress.Message(id: "WifiInstructions1", message: "On the Android, run Bloom Reader, open the menu and choose 'Receive Books from computer'."); _progress.Message(id: "WifiInstructions2", message: "You can do this on as many devices as you like. Make sure each device is connected to the same network as this computer."); }
/// <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 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); }