Example #1
0
        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));
            }
        }
Example #8
0
        /// <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);
            }
        }
Example #9
0
        /// <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 &lt;meta name="creator" content="..."/&gt; (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");
            }
        }
Example #11
0
        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);
            }
        }
Example #13
0
        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);
            }
        }
Example #14
0
        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);
 }