public void CompressDirectory_CompressBookWithExtraHtmlFiles_ExtrasAreIgnored() { // Seeing w:stdPr (part of urn:schemas-microsoft-com:office:word) would cause XmlHtmlConvert.GetXmlDomFromHtml to crash string htmContents = "<html><body><w:sdtPr></w:sdtPr></body></html>"; // Setup // SetupDirectoryWithHtml(kMinimumValidBookHtml, actionsOnFolderBeforeCompressing: folderPath => { // We expect this file to just be passed through, but not parsed string extraHtmDir = Path.Combine(folderPath, "unnecessaryExtraFiles"); Directory.CreateDirectory(extraHtmDir); File.WriteAllText(Path.Combine(extraHtmDir, "causesException.htm"), htmContents); }); if (_bookFolder == null) { Assert.Fail("Test setup error: Test setup failed to initialize bookFolder."); } // System Under Test // using (var bloomPubTempFile = TempFile.WithFilenameInTempFolder("BookCompressorBloomPub" + BookCompressor.BloomPubExtensionWithDot)) { BookCompressor.CompressBookDirectory(bloomPubTempFile.Path, _bookFolder.Path, ""); } // Verification // // Just make sure it didn't crash return; }
private void TestHtmlAfterCompression(string originalBookHtml, Action <string> actionsOnFolderBeforeCompressing = null, Action <string> assertionsOnResultingHtmlString = null, Action <ZipFile> assertionsOnZipArchive = null, Action <ZipFile> assertionsOnRepeat = null) { var testBook = CreateBookWithPhysicalFile(originalBookHtml, bringBookUpToDate: true); var bookFileName = Path.GetFileName(testBook.GetPathHtmlFile()); actionsOnFolderBeforeCompressing?.Invoke(testBook.FolderPath); using (var bloomdTempFile = TempFile.WithFilenameInTempFolder(testBook.Title + BookCompressor.ExtensionForDeviceBloomBook)) { BookCompressor.CompressBookForDevice(bloomdTempFile.Path, testBook, _bookServer, Color.Azure, new NullWebSocketProgress()); var zip = new ZipFile(bloomdTempFile.Path); assertionsOnZipArchive?.Invoke(zip); var newHtml = GetEntryContents(zip, bookFileName); assertionsOnResultingHtmlString?.Invoke(newHtml); if (assertionsOnRepeat != null) { // compress it again! Used for checking important repeatable results using (var extraTempFile = TempFile.WithFilenameInTempFolder(testBook.Title + "2" + BookCompressor.ExtensionForDeviceBloomBook)) { BookCompressor.CompressBookForDevice(extraTempFile.Path, testBook, _bookServer, Color.Azure, new NullWebSocketProgress()); zip = new ZipFile(extraTempFile.Path); assertionsOnRepeat(zip); } } } }
public void GetBytesOfReducedImage_LargeJpgImageReduced() { // LakePendOreille.jpg: JPEG image data, JFIF standard 1.01, ... precision 8, 3264x2448, frames 3 var path = SIL.IO.FileLocator.GetFileDistributedWithApplication(_pathToTestImages, "LakePendOreille.jpg"); var originalBytes = File.ReadAllBytes(path); var reducedBytes = BookCompressor.GetBytesOfReducedImage(path); Assert.Greater(originalBytes.Length, reducedBytes.Length, "LakePendOreille.jpg is reduced from 3264x2448"); using (var tempFile = TempFile.WithExtension(Path.GetExtension(path))) { var oldMetadata = Metadata.FromFile(path); RobustFile.WriteAllBytes(tempFile.Path, reducedBytes); var newMetadata = Metadata.FromFile(tempFile.Path); if (oldMetadata.IsEmpty) { Assert.IsTrue(newMetadata.IsEmpty); } else { Assert.IsFalse(newMetadata.IsEmpty); Assert.AreEqual(oldMetadata.CopyrightNotice, newMetadata.CopyrightNotice, "copyright preserved for LakePendOreille.jpg"); Assert.AreEqual(oldMetadata.Creator, newMetadata.Creator, "creator preserved for LakePendOreille.jpg"); Assert.AreEqual(oldMetadata.License.ToString(), newMetadata.License.ToString(), "license preserved for LakePendOreille.jpg"); } } }
public void GetBytesOfReducedImage_LargePngImageReduced() { // shirtWithTransparentBg.png: PNG image data, 2208 x 2400, 8-bit/color RGBA, non-interlaced var path = SIL.IO.FileLocator.GetFileDistributedWithApplication(_pathToTestImages, "shirt.png"); var originalBytes = File.ReadAllBytes(path); var reducedBytes = BookCompressor.GetBytesOfReducedImage(path); Assert.Greater(originalBytes.Length, reducedBytes.Length, "shirt.png is reduced from 2208x2400"); using (var tempFile = TempFile.WithExtension(Path.GetExtension(path))) { var oldMetadata = Metadata.FromFile(path); RobustFile.WriteAllBytes(tempFile.Path, reducedBytes); var newMetadata = Metadata.FromFile(tempFile.Path); if (oldMetadata.IsEmpty) { Assert.IsTrue(newMetadata.IsEmpty); } else { Assert.IsFalse(newMetadata.IsEmpty); Assert.AreEqual(oldMetadata.CopyrightNotice, newMetadata.CopyrightNotice, "copyright preserved for shirt.png"); Assert.AreEqual(oldMetadata.Creator, newMetadata.Creator, "creator preserved for shirt.png"); Assert.AreEqual(oldMetadata.License.ToString(), newMetadata.License.ToString(), "license preserved for shirt.png"); } } }
public void GetBytesOfReducedImage_SmallJpgImageStaysSame() { // man.jpg: JPEG image data, JFIF standard 1.01, ..., precision 8, 118x154, frames 3 var path = SIL.IO.FileLocator.GetFileDistributedWithApplication(_pathToTestImages, "man.jpg"); var originalBytes = File.ReadAllBytes(path); var reducedBytes = BookCompressor.GetBytesOfReducedImage(path); Assert.AreEqual(originalBytes, reducedBytes, "man.jpg is already small enough (118x154)"); using (var tempFile = TempFile.WithExtension(Path.GetExtension(path))) { var oldMetadata = Metadata.FromFile(path); RobustFile.WriteAllBytes(tempFile.Path, reducedBytes); var newMetadata = Metadata.FromFile(tempFile.Path); if (oldMetadata.IsEmpty) { Assert.IsTrue(newMetadata.IsEmpty); } else { Assert.IsFalse(newMetadata.IsEmpty); Assert.AreEqual(oldMetadata.CopyrightNotice, newMetadata.CopyrightNotice, "copyright preserved for man.jpg"); Assert.AreEqual(oldMetadata.Creator, newMetadata.Creator, "creator preserved for man.jpg"); Assert.AreEqual(oldMetadata.License.ToString(), newMetadata.License.ToString(), "license preserved for man.jpg"); } } }
public void GetBytesOfReducedImage_SmallPngImageMadeTransparent() { // bird.png: PNG image data, 274 x 300, 8-bit/color RGBA, non-interlaced var path = SIL.IO.FileLocator.GetFileDistributedWithApplication(_pathToTestImages, "bird.png"); byte[] originalBytes = File.ReadAllBytes(path); byte[] reducedBytes = BookCompressor.GetBytesOfReducedImage(path); Assert.That(reducedBytes, Is.Not.EqualTo(originalBytes)); // no easy way to check it was made transparent, but should be changed. // Size should not change much. Assert.That(reducedBytes.Length, Is.LessThan(originalBytes.Length * 11 / 10)); Assert.That(reducedBytes.Length, Is.GreaterThan(originalBytes.Length * 9 / 10)); using (var tempFile = TempFile.WithExtension(Path.GetExtension(path))) { var oldMetadata = Metadata.FromFile(path); RobustFile.WriteAllBytes(tempFile.Path, reducedBytes); var newMetadata = Metadata.FromFile(tempFile.Path); if (oldMetadata.IsEmpty) { Assert.IsTrue(newMetadata.IsEmpty); } else { Assert.IsFalse(newMetadata.IsEmpty); Assert.AreEqual(oldMetadata.CopyrightNotice, newMetadata.CopyrightNotice, "copyright preserved for bird.png"); Assert.AreEqual(oldMetadata.Creator, newMetadata.Creator, "creator preserved for bird.png"); Assert.AreEqual(oldMetadata.License.ToString(), newMetadata.License.ToString(), "license preserved for bird.png"); } } }
public void CompressBookForDevice_FileNameIsCorrect() { var testBook = CreateBookWithPhysicalFile(kMinimumValidBookHtml, bringBookUpToDate: true); using (var bloomdTempFile = TempFile.WithFilenameInTempFolder(testBook.Title + BookCompressor.ExtensionForDeviceBloomBook)) { BookCompressor.CompressBookForDevice(bloomdTempFile.Path, testBook, _bookServer, Color.Azure, new NullWebSocketProgress()); Assert.AreEqual(testBook.Title + BookCompressor.ExtensionForDeviceBloomBook, Path.GetFileName(bloomdTempFile.Path)); } }
/// <summary> /// Makes a BloomPack of the specified dir. /// </summary> /// <param name="path">The path to write to. Precondition: Must not exist.</param> internal void MakeBloomPackInternal(string path, string dir, string dirNamePrefix, bool forReaderTools, bool isCollection) { var excludeAudio = true; // don't want audio in bloompack if (isCollection) { BookCompressor.CompressCollectionDirectory(path, dir, dirNamePrefix, forReaderTools, excludeAudio); } else { BookCompressor.CompressBookDirectory(path, dir, dirNamePrefix, forReaderTools, excludeAudio); } }
/// <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> /// <returns>Path to the unzipped .bloomd</returns> public static string CreateBloomDigitalBook(string outputPath, string bookFolderPath, BookServer bookServer, Color backColor, WebSocketProgress progress, TemporaryFolder tempFolder, string creator = "bloom", AndroidPublishSettings settings = null) { var modifiedBook = PrepareBookForBloomReader(bookFolderPath, bookServer, tempFolder, progress, 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.CompressDirectory(outputPath, modifiedBook.FolderPath, "", reduceImages: true, omitMetaJson: false, wrapWithFolder: false, pathToFileForSha: BookStorage.FindBookHtmlInFolder(bookFolderPath)); return(modifiedBook.FolderPath); }
public void GetBytesOfReducedImage_LargePng24bImageReduced() { // lady24b.png: PNG image data, 24bit depth, 3632w x 3872h var path = FileLocator.GetFileDistributedWithApplication(_pathToTestImages, "lady24b.png"); var originalBytes = File.ReadAllBytes(path); var reducedBytes = BookCompressor.GetBytesOfReducedImage(path); // Is it reduced, even tho' we switched from 24bit depth to 32bit depth? Assert.Greater(originalBytes.Length, reducedBytes.Length, "lady24b.png is reduced from 3632x3872"); using (var tempFile = TempFile.WithExtension(Path.GetExtension(path))) { RobustFile.WriteAllBytes(tempFile.Path, reducedBytes); using (var newImage = PalasoImage.FromFileRobustly(tempFile.Path)) Assert.AreEqual(PixelFormat.Format32bppArgb, newImage.Image.PixelFormat, "should have switched to 32bit depth"); } }
public void CompressDirectory_CompressBooksWithBackgroundAudioFiles() { // Setup // var bookHtml = @"<html><head><link rel='stylesheet' href='Basic Book.css' type='text/css'></link></head><body> <div class='bloom-page' id='guid1' data-backgroundaudio='musicfile1.mp3'></div> <div class='bloom-page' id='guid2' data-backgroundaudio='musicfile2.ogg'></div> <div class='bloom-page' id='guid3' data-backgroundaudio='musicfile3.wav'></div> </body></html>" ; SetupDirectoryWithHtml(bookHtml, actionsOnFolderBeforeCompressing: folderPath => { // We expect this file to just be passed through, but not parsed string audioDir = Path.Combine(folderPath, "audio"); Directory.CreateDirectory(audioDir); File.WriteAllText(Path.Combine(audioDir, "musicfile1.mp3"), "dummy mp3 content"); File.WriteAllText(Path.Combine(audioDir, "musicfile2.ogg"), "dummy ogg content"); File.WriteAllText(Path.Combine(audioDir, "musicfile3.wav"), "dummy wav content"); File.WriteAllText(Path.Combine(audioDir, "narration.mp3"), "more dummy mp3 content"); // file should be included (even though not referenced) File.WriteAllText(Path.Combine(audioDir, "narration.wav"), "more dummy wav content"); // file should not be included File.WriteAllText(Path.Combine(folderPath, "temp.tmp"), "dummy temporary file data"); // file should not be included }); // System Under Test // using (var bloomPubTempFile = TempFile.WithFilenameInTempFolder("BookCompressorWithAudio" + BookCompressor.BloomPubExtensionWithDot)) { BookCompressor.CompressBookDirectory(bloomPubTempFile.Path, _bookFolder.Path, ""); // Test by looking at the temp file content. using (var zippedFile = new ZipFile(bloomPubTempFile.Path)) { var count = 0; foreach (ZipEntry zipEntry in zippedFile) { ++count; //Console.Out.WriteLine($"DEBUG: name={zipEntry.Name}, IsFile={zipEntry.IsFile}"); Assert.Contains(zipEntry.Name, new[] { "book/book.htm", "book/audio/musicfile1.mp3", "book/audio/musicfile2.ogg", "book/audio/musicfile3.wav", "book/audio/narration.mp3" }); } Assert.AreEqual(5, count, "Should be five files stored in test .bloompub file"); } } }
/// <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); } }
private void MakeBloomPackInternal(string path, string dir, string dirNamePrefix, bool forReaderTools) { try { if (RobustFile.Exists(path)) { // UI already got permission for this RobustFile.Delete(path); } using (var pleaseWait = new SimpleMessageDialog("Creating BloomPack...", "Bloom")) { try { pleaseWait.Show(); pleaseWait.BringToFront(); Application.DoEvents(); // actually show it Cursor.Current = Cursors.WaitCursor; Logger.WriteEvent("BloomPack path will be " + path + ", made from " + dir + " with rootName " + Path.GetFileName(dir)); var excludeAudio = true; // don't want audio in bloompack BookCompressor.CompressDirectory(path, dir, dirNamePrefix, forReaderTools, excludeAudio); // show it Logger.WriteEvent("Showing BloomPack on disk"); PathUtilities.SelectFileInExplorer(path); Analytics.Track("Create BloomPack"); } finally { Cursor.Current = Cursors.Default; pleaseWait.Close(); } } } catch (Exception e) { ErrorReport.NotifyUserOfProblem(e, "Could not make the BloomPack at " + path); } }
public void CompressDirectory_CompressBooksWithExtraFiles() { // Setup // var bookHtml = @"<html><head><link rel='stylesheet' href='Basic Book.css' type='text/css'></link></head><body> <div class='bloom-page' id='guid1'></div> <div class='bloom-page' id='guid2' data-backgroundaudio='musicfile.ogg'></div> <div class='bloom-page' id='guid3'></div> </body></html>" ; SetupDirectoryWithHtml(bookHtml, actionsOnFolderBeforeCompressing: folderPath => { // Extra top-level book files should get ignored (non-whitelisted extensions). File.WriteAllText(Path.Combine(folderPath, "temp.tmp"), "dummy temporary file data"); File.WriteAllText(Path.Combine(folderPath, "temp.xyz"), "dummy temporary file data"); // Valid entries should get through. File.WriteAllText(Path.Combine(folderPath, "meta.json"), "dummy meta.json file"); File.WriteAllText(Path.Combine(folderPath, "thumbnail.png"), "dummy thumbnail file"); string audioDir = Path.Combine(folderPath, "audio"); Directory.CreateDirectory(audioDir); File.WriteAllText(Path.Combine(audioDir, "musicfile.ogg"), "dummy ogg content"); // These 2 should not be included (non-whitelisted extensions) File.WriteAllText(Path.Combine(audioDir, "midiMusic.mid"), "dummy midi file data"); File.WriteAllText(Path.Combine(audioDir, "other.aif"), "dummy audio file data"); // This subsubfolder should be completely ignored (only 'activities' can have // subsubfolders). string subAudioDir = Path.Combine(audioDir, "audio2"); Directory.CreateDirectory(subAudioDir); File.WriteAllText(Path.Combine(subAudioDir, "other.mp3"), "dummy audio file data"); string videoDir = Path.Combine(folderPath, "video"); Directory.CreateDirectory(videoDir); File.WriteAllText(Path.Combine(videoDir, "signlanguageVid.mp4"), "dummy video content"); // These 2 should not be included (non-whitelisted extensions) File.WriteAllText(Path.Combine(videoDir, "sign.mov"), "dummy movie file data"); File.WriteAllText(Path.Combine(videoDir, "sign.wmv"), "dummy movie file data"); // This subsubfolder should be completely ignored (only 'activities' can have // subsubfolders). string subVideoDir = Path.Combine(videoDir, "video2"); Directory.CreateDirectory(subVideoDir); File.WriteAllText(Path.Combine(subVideoDir, "other.mp4"), "dummy video file data"); // We don't know all the file types that might be in a widget, // so we expect to pass through every file and folder within the 'activities' folder. string activityDir = Path.Combine(folderPath, "activities"); Directory.CreateDirectory(activityDir); string widgetDir = Path.Combine(activityDir, "My Widget"); Directory.CreateDirectory(widgetDir); File.WriteAllText(Path.Combine(widgetDir, "odd.html"), "dummy html content"); File.WriteAllText(Path.Combine(widgetDir, "strange.js"), "dummy js content"); var widgetResources = Path.Combine(widgetDir, "Resources"); Directory.CreateDirectory(widgetResources); File.WriteAllText(Path.Combine(widgetResources, "includable.blob"), "some unknown blob-type file contents"); // We expect this folder to be excluded completely (non-whitelisted book subfolder). string bookWithinABookDir = Path.Combine(folderPath, "Book Within A Book"); Directory.CreateDirectory(bookWithinABookDir); File.WriteAllText(Path.Combine(bookWithinABookDir, "extra book.html"), "dummy html content"); }); // System Under Test // using (var bloomPubTempFile = TempFile.WithFilenameInTempFolder("BookCompressorWithExtraFiles" + BookCompressor.BloomPubExtensionWithDot)) { BookCompressor.CompressBookDirectory(bloomPubTempFile.Path, _bookFolder.Path, ""); // Test by looking at the temp file content. using (var zippedFile = new ZipFile(bloomPubTempFile.Path)) { var count = 0; foreach (ZipEntry zipEntry in zippedFile) { ++count; //Console.Out.WriteLine($"DEBUG: name={zipEntry.Name}, IsFile={zipEntry.IsFile}"); Assert.Contains(zipEntry.Name, new[] { "book/book.htm", "book/meta.json", "book/thumbnail.png", "book/audio/musicfile.ogg", "book/video/signlanguageVid.mp4", "book/activities/My Widget/odd.html", "book/activities/My Widget/strange.js", "book/activities/My Widget/Resources/includable.blob" }); } Assert.AreEqual(8, count, "Should be eight files stored in test .bloompub file"); } } }
public void RunCompressDirectoryTest(string outputPath, bool forReaderTools = false) { BookCompressor.CompressDirectory(outputPath, TestFolderPath, "", forReaderTools); }