/// <summary> /// Create a Bloom Digital book (the zipped .bloomd file) as used by BloomReader (and Bloom Library etc) /// </summary> /// <param name="outputPath">The path to create the zipped .bloomd 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 <meta name="creator" content="..."/> (defaults to "bloom")</param> /// <param name="isTemplateBook"></param> /// <param name="settings"></param> /// <returns>Path to the unzipped .bloomd</returns> public static string CreateBloomPub(string outputPath, string bookFolderPath, BookServer bookServer, Color backColor, WebSocketProgress 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, backColor, modifiedBook.FolderPath, 256); BookCompressor.CompressBookDirectory(outputPath, modifiedBook.FolderPath, "", reduceImages: true, omitMetaJson: false, wrapWithFolder: false, pathToFileForSha: BookStorage.FindBookHtmlInFolder(bookFolderPath)); return(modifiedBook.FolderPath); }
private void ProcessVideosInTempDirectory(string destDirName) { var htmlFilePath = BookStorage.FindBookHtmlInFolder(destDirName); if (string.IsNullOrEmpty(htmlFilePath)) { return; } var xmlDomFromHtmlFile = XmlHtmlConverter.GetXmlDomFromHtmlFile(htmlFilePath); var domForVideoProcessing = new HtmlDom(xmlDomFromHtmlFile); var videoContainerElements = HtmlDom.SelectChildVideoElements(domForVideoProcessing.RawDom.DocumentElement).Cast <XmlElement>(); if (!videoContainerElements.Any()) { return; } SignLanguageApi.ProcessVideos(videoContainerElements, destDirName); XmlHtmlConverter.SaveDOMAsHtml5(domForVideoProcessing.RawDom, htmlFilePath); }
[TestCase("foobar", "avoid conflict.html")] // only this one file with conflict in the name public void FindBookHtmlInFolder_MayHaveOtherFiles_ChoosesCorrectOne(string folderName, string expected, string decoy1 = null, string decoy2 = null) { using (var outerFolder = new TemporaryFolder()) // intentionally using different name each time to avoid conflicts when tests run in parallel { using (var folder = new TemporaryFolder(outerFolder, folderName)) { File.CreateText(folder.Combine(expected)); if (decoy1 != null) { File.CreateText(folder.Combine(decoy1)); } if (decoy2 != null) { File.CreateText(folder.Combine(decoy2)); } var path = BookStorage.FindBookHtmlInFolder(folder.Path); Assert.AreEqual(expected, Path.GetFileName(path)); } } }
private void ReplaceBookWithTemplate(ZipFile zip, string directory, string rootName) { var bookFile = BookStorage.FindBookHtmlInFolder(directory); if (string.IsNullOrEmpty(bookFile)) { return; } var text = File.ReadAllText(bookFile, Encoding.UTF8); // Note that we're getting rid of preceding newline but not following one. Hopefully we cleanly remove a whole line. // I'm not sure the </meta> ever occurs in html files, but just in case we'll match if present. var regex = new Regex("\\s*<meta\\s+name=(['\\\"])lockedDownAsShell\\1 content=(['\\\"])true\\2>(</meta>)? *"); var match = regex.Match(text); if (!match.Success) { return; // nothing to remove } var newText = text.Substring(0, match.Index) + text.Substring(match.Index + match.Length); var zipName = Path.Combine(rootName, Path.GetFileName(directory), Path.GetFileName(bookFile)); zip.RemoveEntry(zipName); zip.AddEntry(zipName, newText); }
private string UploadBook(string bookFolder, IProgress progress, out string parseId, string pdfToInclude = null, bool excludeAudio = true) { // Books in the library should generally show as locked-down, so new users are automatically in localization mode. // Occasionally we may want to upload a new authoring template, that is, a 'book' that is suitableForMakingShells. // Such books must never be locked. // So, typically we will try to lock it. What we want to do is Book.RecordedAsLockedDown = true; Book.Save(). // But all kinds of things have to be set up before we can create a Book. So we duplicate a few bits of code. var htmlFile = BookStorage.FindBookHtmlInFolder(bookFolder); bool wasLocked = false; bool allowLocking = false; HtmlDom domForLocking = null; var metaDataText = MetaDataText(bookFolder); var metadata = BookMetaData.FromString(metaDataText); if (!string.IsNullOrEmpty(htmlFile)) { var xmlDomFromHtmlFile = XmlHtmlConverter.GetXmlDomFromHtmlFile(htmlFile, false); domForLocking = new HtmlDom(xmlDomFromHtmlFile); wasLocked = domForLocking.RecordedAsLockedDown; allowLocking = !metadata.IsSuitableForMakingShells; if (allowLocking && !wasLocked) { domForLocking.RecordAsLockedDown(true); XmlHtmlConverter.SaveDOMAsHtml5(domForLocking.RawDom, htmlFile); } } string s3BookId; try { // In case we somehow have a book with no ID, we must have one to upload it. if (string.IsNullOrEmpty(metadata.Id)) { metadata.Id = Guid.NewGuid().ToString(); } // And similarly it should have SOME title. if (string.IsNullOrEmpty(metadata.Title)) { metadata.Title = Path.GetFileNameWithoutExtension(bookFolder); } metadata.SetUploader(UserId); s3BookId = S3BookId(metadata); metadata.DownloadSource = s3BookId; // Any updated ID at least needs to become a permanent part of the book. // The file uploaded must also contain the correct DownloadSource data, so that it can be used // as an 'order' to download the book. // It simplifies unit testing if the metadata file is also updated with the uploadedBy value. // Not sure if there is any other reason to do it (or not do it). // For example, do we want to send/receive who is the latest person to upload? metadata.WriteToFolder(bookFolder); // The metadata is also a book order...but we need it on the server with the desired file name, // because we can't rename on download. The extension must be the one Bloom knows about, // and we want the file name to indicate which book, so use the name of the book folder. var metadataPath = BookMetaData.MetaDataPath(bookFolder); var orderPath = Path.Combine(bookFolder, Path.GetFileName(bookFolder) + BookOrderExtension); RobustFile.Copy(metadataPath, orderPath, true); parseId = ""; try { _s3Client.UploadBook(s3BookId, bookFolder, progress, pdfToInclude, excludeAudio); metadata.BaseUrl = _s3Client.BaseUrl; metadata.BookOrder = _s3Client.BookOrderUrlOfRecentUpload; progress.WriteStatus(LocalizationManager.GetString("PublishTab.Upload.UploadingBookMetadata", "Uploading book metadata", "In this step, Bloom is uploading things like title, languages, and topic tags to the BloomLibrary.org database.")); // Do this after uploading the books, since the ThumbnailUrl is generated in the course of the upload. var response = _parseClient.SetBookRecord(metadata.WebDataJson); parseId = response.ResponseUri.LocalPath; int index = parseId.LastIndexOf('/'); parseId = parseId.Substring(index + 1); if (parseId == "books") { // For NEW books the response URL is useless...need to do a new query to get the ID. var json = _parseClient.GetSingleBookRecord(metadata.Id); parseId = json.objectId.Value; } // if (!UseSandbox) // don't make it seem like there are more uploads than their really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Success", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title } }); } } catch (WebException e) { DisplayNetworkUploadProblem(e, progress); if (!UseSandbox) // don't make it seem like there are more upload failures than their really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } return(""); } catch (AmazonS3Exception e) { if (e.Message.Contains("The difference between the request time and the current time is too large")) { progress.WriteError(LocalizationManager.GetString("PublishTab.Upload.TimeProblem", "There was a problem uploading your book. This is probably because your computer is set to use the wrong timezone or your system time is badly wrong. See http://www.di-mgt.com.au/wclock/help/wclo_setsysclock.html for how to fix this.")); if (!UseSandbox) { Analytics.Track("UploadBook-Failure-SystemTime"); } } else { DisplayNetworkUploadProblem(e, progress); if (!UseSandbox) { // don't make it seem like there are more upload failures than their really are if this a tester pushing to the sandbox Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } } return(""); } catch (AmazonServiceException e) { DisplayNetworkUploadProblem(e, progress); if (!UseSandbox) // don't make it seem like there are more upload failures than their really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } return(""); } catch (Exception e) { progress.WriteError(LocalizationManager.GetString("PublishTab.Upload.UploadProblemNotice", "There was a problem uploading your book. You may need to restart Bloom or get technical help.")); progress.WriteError(e.Message.Replace("{", "{{").Replace("}", "}}")); progress.WriteVerbose(e.StackTrace); if (!UseSandbox) // don't make it seem like there are more upload failures than their really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } return(""); } } finally { if (domForLocking != null && allowLocking && !wasLocked) { domForLocking.RecordAsLockedDown(false); XmlHtmlConverter.SaveDOMAsHtml5(domForLocking.RawDom, htmlFile); } } return(s3BookId); }
private string UploadBook(string bookFolder, IProgress progress, out string parseId, string pdfToInclude = null, ISet <string> audioFilesToInclude = null, IEnumerable <string> videoFilesToInclude = null, string[] languages = null, CollectionSettings collectionSettings = null) { // Books in the library should generally show as locked-down, so new users are automatically in localization mode. // Occasionally we may want to upload a new authoring template, that is, a 'book' that is suitableForMakingShells. // Such books must never be locked. // So, typically we will try to lock it. What we want to do is Book.RecordedAsLockedDown = true; Book.Save(). // But all kinds of things have to be set up before we can create a Book. So we duplicate a few bits of code. var htmlFile = BookStorage.FindBookHtmlInFolder(bookFolder); bool wasLocked = false; bool allowLocking = false; HtmlDom domForLocking = null; var metaDataText = MetaDataText(bookFolder); var metadata = BookMetaData.FromString(metaDataText); if (!String.IsNullOrEmpty(htmlFile)) { var xmlDomFromHtmlFile = XmlHtmlConverter.GetXmlDomFromHtmlFile(htmlFile, false); domForLocking = new HtmlDom(xmlDomFromHtmlFile); wasLocked = domForLocking.RecordedAsLockedDown; allowLocking = !metadata.IsSuitableForMakingShells; if (allowLocking && !wasLocked) { domForLocking.RecordAsLockedDown(true); XmlHtmlConverter.SaveDOMAsHtml5(domForLocking.RawDom, htmlFile); } } string s3BookId; try { // In case we somehow have a book with no ID, we must have one to upload it. if (String.IsNullOrEmpty(metadata.Id)) { metadata.Id = Guid.NewGuid().ToString(); } // And similarly it should have SOME title. if (String.IsNullOrEmpty(metadata.Title)) { metadata.Title = Path.GetFileNameWithoutExtension(bookFolder); } metadata.SetUploader(UserId); s3BookId = S3BookId(metadata); #if DEBUG // S3 URL can be reasonably deduced, as long as we have the S3 ID, so print that out in Debug mode. // Format: $"https://s3.amazonaws.com/BloomLibraryBooks{isSandbox}/{s3BookId}/{title}" // Example: https://s3.amazonaws.com/BloomLibraryBooks-Sandbox/[email protected]/8d0d9043-a1bb-422d-aa5b-29726cdcd96a/AutoSplit+Timings var msgBookId = "s3BookId: " + s3BookId; progress.WriteMessage(msgBookId); #endif metadata.DownloadSource = s3BookId; // If the collection has a default bookshelf, make sure the book has that tag. // Also make sure it doesn't have any other bookshelf tags (which would typically be // from a previous default bookshelf upload), including a duplicate of the one // we may be about to add. var tags = (metadata.Tags ?? new string[0]).Where(t => !t.StartsWith("bookshelf:")); if (!String.IsNullOrEmpty(collectionSettings?.DefaultBookshelf)) { tags = tags.Concat(new [] { "bookshelf:" + collectionSettings.DefaultBookshelf }); } metadata.Tags = tags.ToArray(); // Any updated ID at least needs to become a permanent part of the book. // The file uploaded must also contain the correct DownloadSource data, so that it can be used // as an 'order' to download the book. // It simplifies unit testing if the metadata file is also updated with the uploadedBy value. // Not sure if there is any other reason to do it (or not do it). // For example, do we want to send/receive who is the latest person to upload? metadata.WriteToFolder(bookFolder); // The metadata is also a book order...but we need it on the server with the desired file name, // because we can't rename on download. The extension must be the one Bloom knows about, // and we want the file name to indicate which book, so use the name of the book folder. var metadataPath = BookMetaData.MetaDataPath(bookFolder); RobustFile.Copy(metadataPath, BookInfo.BookOrderPath(bookFolder), true); parseId = ""; try { _s3Client.UploadBook(s3BookId, bookFolder, progress, pdfToInclude, audioFilesToInclude, videoFilesToInclude, languages); metadata.BaseUrl = _s3Client.BaseUrl; metadata.BookOrder = _s3Client.BookOrderUrlOfRecentUpload; var metaMsg = LocalizationManager.GetString("PublishTab.Upload.UploadingBookMetadata", "Uploading book metadata", "In this step, Bloom is uploading things like title, languages, and topic tags to the BloomLibrary.org database."); if (IsDryRun) { metaMsg = "(Dry run) Would upload book metadata"; // TODO: localize? } progress.WriteStatus(metaMsg); // Do this after uploading the books, since the ThumbnailUrl is generated in the course of the upload. if (!IsDryRun) { var response = ParseClient.SetBookRecord(metadata.WebDataJson); parseId = response.ResponseUri.LocalPath; int index = parseId.LastIndexOf('/'); parseId = parseId.Substring(index + 1); if (parseId == "books") { // For NEW books the response URL is useless...need to do a new query to get the ID. var json = ParseClient.GetSingleBookRecord(metadata.Id); parseId = json.objectId.Value; } // if (!UseSandbox) // don't make it seem like there are more uploads than their really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Success", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title } }); } } } catch (WebException e) { DisplayNetworkUploadProblem(e, progress); if (IsProductionRun) // don't make it seem like there are more upload failures than their really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } return(""); } catch (AmazonS3Exception e) { if (e.Message.Contains("The difference between the request time and the current time is too large")) { progress.WriteError(LocalizationManager.GetString("PublishTab.Upload.TimeProblem", "There was a problem uploading your book. This is probably because your computer is set to use the wrong timezone or your system time is badly wrong. See http://www.di-mgt.com.au/wclock/help/wclo_setsysclock.html for how to fix this.")); if (IsProductionRun) { Analytics.Track("UploadBook-Failure-SystemTime"); } } else { DisplayNetworkUploadProblem(e, progress); if (IsProductionRun) { // don't make it seem like there are more upload failures than there really are if this a tester pushing to the sandbox Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } } return(""); } catch (AmazonServiceException e) { DisplayNetworkUploadProblem(e, progress); if (IsProductionRun) // don't make it seem like there are more upload failures than there really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } return(""); } catch (Exception e) { var msg1 = LocalizationManager.GetString("PublishTab.Upload.UploadProblemNotice", "There was a problem uploading your book. You may need to restart Bloom or get technical help."); var msg2 = e.Message.Replace("{", "{{").Replace("}", "}}"); progress.WriteError(msg1); progress.WriteError(msg2); progress.WriteVerbose(e.StackTrace); if (IsProductionRun) // don't make it seem like there are more upload failures than there really are if this a tester pushing to the sandbox { Analytics.Track("UploadBook-Failure", new Dictionary <string, string>() { { "url", metadata.BookOrder }, { "title", metadata.Title }, { "error", e.Message } }); } return(""); } } finally { if (domForLocking != null && allowLocking && !wasLocked) { domForLocking.RecordAsLockedDown(false); XmlHtmlConverter.SaveDOMAsHtml5(domForLocking.RawDom, htmlFile); } } return(s3BookId); }
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); }
/// <summary> /// Adds a directory, along with all files and subdirectories, to the ZipStream. /// </summary> /// <param name="directoryPath">The directory to add recursively</param> /// <param name="zipStream">The ZipStream to which the files and directories will be added</param> /// <param name="dirNameOffset">This number of characters will be removed from the full directory or file name /// before creating the zip entry name</param> /// <param name="dirNamePrefix">string to prefix to the zip entry name</param> /// <param name="forReaderTools">If True, then some pre-processing will be done to the contents of decodable /// and leveled readers before they are added to the ZipStream</param> /// <remarks>Protected for testing purposes</remarks> protected static void CompressDirectory(string directoryPath, ZipOutputStream zipStream, int dirNameOffset, string dirNamePrefix, bool forReaderTools) { if (Path.GetFileName(directoryPath).ToLowerInvariant() == "audio") { return; // don't want audio in bloompack. todo: test } var files = Directory.GetFiles(directoryPath); var bookFile = BookStorage.FindBookHtmlInFolder(directoryPath); foreach (var filePath in files) { if (_excludedFileExtensionsLowerCase.Contains(Path.GetExtension(filePath.ToLowerInvariant()))) { continue; // BL-2246: skip putting this one into the BloomPack } if (Path.GetFileName(filePath).StartsWith(BookStorage.PrefixForCorruptHtmFiles)) { continue; } FileInfo fi = new FileInfo(filePath); var entryName = dirNamePrefix + filePath.Substring(dirNameOffset); // Makes the name in zip based on the folder entryName = ZipEntry.CleanName(entryName); // Removes drive from name and fixes slash direction ZipEntry newEntry = new ZipEntry(entryName) { DateTime = fi.LastWriteTime }; newEntry.IsUnicodeText = true; // encode filename and comment in UTF8 byte[] modifiedContent = {}; // if this is a ReaderTools book, call GetBookReplacedWithTemplate() to get the contents if (forReaderTools && (bookFile == filePath)) { modifiedContent = Encoding.UTF8.GetBytes(GetBookReplacedWithTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else if (forReaderTools && (Path.GetFileName(filePath) == "meta.json")) { modifiedContent = Encoding.UTF8.GetBytes(GetMetaJsonModfiedForTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else { newEntry.Size = fi.Length; } zipStream.PutNextEntry(newEntry); if (modifiedContent.Length > 0) { using (var memStream = new MemoryStream(modifiedContent)) { StreamUtils.Copy(memStream, zipStream, new byte[modifiedContent.Length]); } } else { // Zip the file in buffered chunks byte[] buffer = new byte[4096]; using (var streamReader = RobustFile.OpenRead(filePath)) { StreamUtils.Copy(streamReader, zipStream, buffer); } } zipStream.CloseEntry(); } var folders = Directory.GetDirectories(directoryPath); foreach (var folder in folders) { var dirName = Path.GetFileName(folder); if ((dirName == null) || (dirName.ToLowerInvariant() == "sample texts")) { continue; // Don't want to bundle these up } CompressDirectory(folder, zipStream, dirNameOffset, dirNamePrefix, forReaderTools); } }
public static Book.Book PrepareBookForBloomReader(string bookFolderPath, BookServer bookServer, TemporaryFolder temp, WebSocketProgress progress, string creator = "bloom", 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); // 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.CollectionSettings.Language2.Iso639Code); } 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.CollectionSettings.Language1.Iso639Code }; PublishModel.RemoveUnwantedLanguageData(modifiedBook.OurHtmlDom, languagesToInclude, modifiedBook.CollectionSettings.Language2.Iso639Code); } // 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; } modifiedBook.RemoveBlankPages(settings?.LanguagesToInclude); // See https://issues.bloomlibrary.org/youtrack/issue/BL-6835. RemoveInvisibleImageElements(modifiedBook); modifiedBook.Storage.CleanupUnusedImageFiles(keepFilesForEditing: false); if (RobustFile.Exists(Path.Combine(modifiedBookFolderPath, "placeHolder.png"))) { RobustFile.Delete(Path.Combine(modifiedBookFolderPath, "placeHolder.png")); } modifiedBook.Storage.CleanupUnusedAudioFiles(isForPublish: true); modifiedBook.RemoveObsoleteAudioMarkup(); modifiedBook.Storage.CleanupUnusedVideoFiles(); // 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 modifiedBook.UpdateMetadataFeatures( isBlindEnabled: true, isSignLanguageEnabled: true, isTalkingBookEnabled: true); modifiedBook.SetAnimationDurationsFromAudioDurations(); modifiedBook.OurHtmlDom.SetMedia("bloomReader"); modifiedBook.OurHtmlDom.AddOrReplaceMetaElement("bloom-digital-creator", creator); EmbedFonts(modifiedBook, progress, fontsUsed, new FontFileFinder()); 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); modifiedBook.Save(); return(modifiedBook); }