private PalasoImage MakeSampleTifImage(string path) { var x = new Bitmap(kSampleImageDimension, kSampleImageDimension); x.Save(path, ImageFormat.Tiff); return(PalasoImage.FromFileRobustly(path)); }
//Up through Bloom 3.0, we would make white areas transparent when importing images, in order to make them //look good against the colored background of a book cover. //This caused problems with some PDF viewers, so in Bloom 3.1, we switched to only making them transparent at runtime. //This method allows us to undo that transparency-making. public static void RemoveTransparencyOfImagesInFolder(string folderPath, IProgress progress) { var imageFiles = Directory.GetFiles(folderPath, "*.png"); int completed = 0; foreach (string path in imageFiles) { if (Path.GetFileName(path).ToLowerInvariant() == "placeholder.png") { return; } progress.ProgressIndicator.PercentCompleted = (int)(100.0 * (float)completed / (float)imageFiles.Length); using (var pi = PalasoImage.FromFileRobustly(path)) { // If the image isn't jpeg, and we can't be sure it's already opaque, change the // image to be opaque. As explained above, some PDF viewers don't handle transparent // images very well. if (!AppearsToBeJpeg(pi) && !IsIndexedAndOpaque(pi.Image)) { RemoveTransparency(pi, path, progress); } } completed++; } }
private PalasoImage MakeSampleJpegImage(string path) { var x = new Bitmap(kSampleImageDimension, kSampleImageDimension); x.Save(path, ImageFormat.Jpeg); //nb: even if we reload the image from the file, the rawformat will be memory bitmap, not jpg as we'd wish return(PalasoImage.FromFileRobustly(path)); }
protected void MakeSamplePngImageWithMetadata(string path, int width = 10, int height = 10) { var x = new Bitmap(width, height); RobustImageIO.SaveImage(x, path, ImageFormat.Png); x.Dispose(); using (var img = PalasoImage.FromFileRobustly(path)) { img.Metadata.Creator = "joe"; img.Metadata.CopyrightNotice = "Copyright 1999 by me"; RetryUtility.Retry(() => img.SaveUpdatedMetadataIfItMakesSense()); } }
public void ClipboardRoundTripWorks_Bmp() { var imagePath = GetPathToImage("PasteHS.bmp"); using (var image = PalasoImage.FromFileRobustly(imagePath)) { PortableClipboard.CopyImageToClipboard(image); using (var resultingImage = PortableClipboard.GetImageFromClipboard()) { // There is no working PalasoImage.Equals(), so just try a few properties Assert.AreEqual(image.FileName, resultingImage.FileName); Assert.AreEqual(image.Image.Size, resultingImage.Image.Size); Assert.AreEqual(image.Image.Flags, resultingImage.Image.Flags); } } }
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"); } }
/// <summary> /// Gets the PalasoImage info from the image's filename and folder. If there is a problem, it will return null. /// </summary> /// <param name="folderPath"></param> /// <param name="imageFilePath"></param> /// <returns></returns> public static PalasoImage GetImageInfoSafelyFromFilePath(string folderPath, string imageFilePath) { //enhance: this all could be done without loading the image into memory //could just deal with the metadata //e.g., var metadata = Metadata.FromFile(path) var path = Path.Combine(folderPath, imageFilePath); try { return(PalasoImage.FromFileRobustly(path)); } catch (CorruptFileException e) { ErrorReport.NotifyUserOfProblem(e, "Bloom ran into a problem while trying to read the metadata portion of this image, " + path); return(null); } }
// Make a thumbnail of the input image. newWidth and newHeight are both limits; the image will not // be larger than original, but if necessary will be shrunk to fit within the indicated rectangle. // If parameter 'backColor' is not Empty, we fill the background of the thumbnail with that color. public static bool GenerateThumbnail(string originalPath, string pathToProcessedImage, int newWidth, Color backColor) { using (var originalImage = PalasoImage.FromFileRobustly(originalPath)) { // check if it needs resized if (originalImage.Image.Width <= newWidth) { return(false); } // calculate dimensions var newW = (originalImage.Image.Width > newWidth) ? newWidth : originalImage.Image.Width; // allow for proper rounding from the division var newH = (newW * originalImage.Image.Height + (originalImage.Image.Width / 2)) / originalImage.Image.Width; var thumbnail = new Bitmap(newW, newH); var g = Graphics.FromImage(thumbnail); if (backColor != Color.Empty) { using (var brush = new SolidBrush(backColor)) { g.FillRectangle(brush, new Rectangle(0, 0, newW, newH)); } } Image imageToDraw = originalImage.Image; bool useOriginalImage = ImageUtils.AppearsToBeJpeg(originalImage); if (!useOriginalImage) { imageToDraw = MakePngBackgroundTransparent(originalImage); } var destRect = new Rectangle(0, 0, newW, newH); // Note the image size may change when the background is made transparent. // See https://silbloom.myjetbrains.com/youtrack/issue/BL-5632. g.DrawImage(imageToDraw, destRect, new Rectangle(0, 0, imageToDraw.Width, imageToDraw.Height), GraphicsUnit.Pixel); if (!useOriginalImage) { imageToDraw.Dispose(); } RobustImageIO.SaveImage(thumbnail, pathToProcessedImage); } return(true); }
public void ClipboardRoundTripWorks_GetsExistingMetadata() { var imagePath = GetPathToImage("AOR_EAG00864.png"); using (var image = PalasoImage.FromFileRobustly(imagePath)) { var preCopyLicense = image.Metadata.License.Token; var preCopyCollectionUri = image.Metadata.CollectionUri; PortableClipboard.CopyImageToClipboard(image); using (var resultingImage = PortableClipboard.GetImageFromClipboard()) { // Test that the same metadata came through Assert.IsTrue(resultingImage.Metadata.IsMinimallyComplete); Assert.AreEqual(preCopyLicense, resultingImage.Metadata.License.Token); Assert.AreEqual(preCopyCollectionUri, resultingImage.Metadata.CollectionUri); Assert.AreEqual(image.Image.Flags, resultingImage.Image.Flags); } } }
public static void ProcessAndSaveImageIntoFolder_SimpleImageHasTransparentBackground_ImageNotConvertedAndFileSizeNotIncreased() { var inputPath = SIL.IO.FileLocator.GetFileDistributedWithApplication(_pathToTestImages, "shirtWithTransparentBg.png"); var originalFileSize = new FileInfo(inputPath).Length; using (var image = PalasoImage.FromFileRobustly(inputPath)) { using (var folder = new TemporaryFolder("TransparentPngTest")) { var fileName = ImageUtils.ProcessAndSaveImageIntoFolder(image, folder.Path, false); Assert.AreEqual(".png", Path.GetExtension(fileName)); var outputPath = folder.Combine(fileName); using (var result = Image.FromFile(outputPath)) { Assert.AreEqual(ImageFormat.Png, result.RawFormat); Assert.That(originalFileSize <= new FileInfo(outputPath).Length); } } } }
private static void ProcessAndSaveImageIntoFolder_AndTestResults(string testImageName, ImageFormat expectedOutputFormat) { var inputPath = SIL.IO.FileLocator.GetFileDistributedWithApplication(_pathToTestImages, testImageName); using (var image = PalasoImage.FromFileRobustly(inputPath)) { using (var folder = new TemporaryFolder()) { var fileName = ImageUtils.ProcessAndSaveImageIntoFolder(image, folder.Path, false); Assert.AreEqual(expectedOutputFormat == ImageFormat.Jpeg ? ".jpg" : ".png", Path.GetExtension(fileName)); var outputPath = folder.Combine(fileName); using (var img = Image.FromFile(outputPath)) { Assert.AreEqual(expectedOutputFormat, img.RawFormat); } var alternativeThatShouldNotBeThere = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + (expectedOutputFormat.Equals(ImageFormat.Jpeg) ? ".png" : ".jpg")); Assert.IsFalse(File.Exists(alternativeThatShouldNotBeThere), "Did not expect to have the file " + alternativeThatShouldNotBeThere); } } }
private static bool GenerateThumbnail(string originalPath, string pathToProcessedImage, int newWidth) { using (var originalImage = PalasoImage.FromFileRobustly(originalPath)) { // check if it needs resized if (originalImage.Image.Width <= newWidth) { return(false); } // calculate dimensions var newW = (originalImage.Image.Width > newWidth) ? newWidth : originalImage.Image.Width; var newH = newW * originalImage.Image.Height / originalImage.Image.Width; using (var newImg = originalImage.Image.GetThumbnailImage(newW, newH, () => false, IntPtr.Zero)) { SIL.IO.RobustIO.SaveImage(newImg, pathToProcessedImage); } } return(true); }
//Up through Bloom 3.0, we would make white areas transparent when importing images, in order to make them //look good against the colored background of a book cover. //This caused problems with some PDF viewers, so in Bloom 3.1, we switched to only making them transparent at runtime. //This method allows us to undo that transparency-making. public static void RemoveTransparencyOfImagesInFolder(string folderPath, IProgress progress) { var imageFiles = Directory.GetFiles(folderPath, "*.png"); int completed = 0; foreach (string path in imageFiles) { if (Path.GetFileName(path).ToLowerInvariant() == "placeholder.png") { return; } progress.ProgressIndicator.PercentCompleted = (int)(100.0 * (float)completed / (float)imageFiles.Length); using (var pi = PalasoImage.FromFileRobustly(path)) { if (!AppearsToBeJpeg(pi)) { RemoveTransparency(pi, path, progress); } } completed++; } }
public static PalasoImage GetImageFromClipboard() { // N.B.: PalasoImage does not handle .svg files #if MONO if (GtkContainsImage()) { return(PalasoImage.FromImage(GtkGetImage())); } if (GtkContainsText()) { //REVIEW: I can find no documentation on GtkClipboard. If ContainsText means we have a file // path, then it would be better to do PalasoImage.FromFileRobustly(); on the file path return(PalasoImage.FromImage(GtkGetImageFromText())); } return(null); #else var dataObject = Clipboard.GetDataObject(); if (dataObject == null) { return(null); } var textData = String.Empty; if (dataObject.GetDataPresent(DataFormats.UnicodeText)) { textData = dataObject.GetData(DataFormats.UnicodeText) as String; } if (Clipboard.ContainsImage()) { PalasoImage plainImage = null; try { plainImage = PalasoImage.FromImage(Clipboard.GetImage()); // this method won't copy any metadata var haveFileUrl = !String.IsNullOrEmpty(textData) && RobustFile.Exists(textData); // If we have an image on the clipboard, and we also have text that is a valid url to an image file, // use the url to create a PalasoImage (which will pull in any metadata associated with the image too) if (haveFileUrl) { var imageWithPathAndMaybeMetadata = PalasoImage.FromFileRobustly(textData); plainImage.Dispose(); //important: don't do this until we've successfully created the imageWithPathAndMaybeMetadata return(imageWithPathAndMaybeMetadata); } else { return(plainImage); } } catch (Exception e) { Logger.WriteEvent("PortableClipboard.GetImageFromClipboard() failed with message " + e.Message); return(plainImage); // at worst, we should return null; if FromFile() failed, we return an image } } // the ContainsImage() returns false when copying an PNG from MS Word // so here we explicitly ask for a PNG and see if we can convert it. if (dataObject.GetDataPresent("PNG")) { var o = dataObject.GetData("PNG") as Stream; try { return(PalasoImage.FromImage(Image.FromStream(o))); } catch (Exception) {} } //People can do a "copy" from the WIndows Photo Viewer but what it puts on the clipboard is a path, not an image if (dataObject.GetDataPresent(DataFormats.FileDrop)) { //This line gets all the file paths that were selected in explorer string[] files = dataObject.GetData(DataFormats.FileDrop) as string[]; if (files == null) { return(null); } foreach (var file in files.Where(f => RobustFile.Exists(f))) { try { return(PalasoImage.FromFileRobustly(file)); } catch (Exception) {} } return(null); //not an image } if (!Clipboard.ContainsText() || !RobustFile.Exists(Clipboard.GetText())) { return(null); } try { return(PalasoImage.FromImage(Image.FromStream(new FileStream(Clipboard.GetText(), FileMode.Open)))); } catch (Exception) {} return(null); #endif }
/// <summary> /// For electronic books, we want to minimize the actual size of images since they'll /// be displayed on small screens anyway. So before zipping up the file, we replace its /// bytes with the bytes of a reduced copy of itself. If the original image is already /// small enough, we return its bytes directly. /// We also make png images have transparent backgrounds. This is currently only necessary /// for cover pages, but it's an additional complication to detect which those are, /// and doesn't seem likely to cost much extra to do. /// </summary> /// <returns>The bytes of the (possibly) reduced image.</returns> internal static byte[] GetBytesOfReducedImage(string filePath) { using (var originalImage = PalasoImage.FromFileRobustly(filePath)) { var image = originalImage.Image; int originalWidth = image.Width; int originalHeight = image.Height; var appearsToBeJpeg = ImageUtils.AppearsToBeJpeg(originalImage); if (originalWidth > kMaxWidth || originalHeight > kMaxHeight || !appearsToBeJpeg) { // Preserve the aspect ratio float scaleX = (float)kMaxWidth / (float)originalWidth; float scaleY = (float)kMaxHeight / (float)originalHeight; // no point in ever expanding, even if we're making a new image just for transparency. float scale = Math.Min(1.0f, Math.Min(scaleX, scaleY)); // New width and height maintaining the aspect ratio int newWidth = (int)(originalWidth * scale); int newHeight = (int)(originalHeight * scale); var imagePixelFormat = image.PixelFormat; switch (imagePixelFormat) { // These three formats are not supported for bitmaps to be drawn on using Graphics.FromImage. // So use the default bitmap format. // Enhance: if these are common it may be worth research to find out whether there are better options. // - possibly the 'reduced' image might not be reduced...even though smaller, the indexed format // might be so much more efficient that it is smaller. However, even if that is true, it doesn't // necessarily follow that it takes less memory to render on the device. So it's not obvious that // we should keep the original just because it's a smaller file. // - possibly we don't need a 32-bit bitmap? Unfortunately the 1bpp/4bpp/8bpp only tells us // that the image uses two, 16, or 256 distinct colors, not what they are or what precision they have. case PixelFormat.Format1bppIndexed: case PixelFormat.Format4bppIndexed: case PixelFormat.Format8bppIndexed: imagePixelFormat = PixelFormat.Format32bppArgb; break; } // OTOH, always using 32-bit format for .png files keeps us from having problems in BloomReader // like BL-5740 (where 24bit format files came out in BR with black backgrounds). if (!appearsToBeJpeg) { imagePixelFormat = PixelFormat.Format32bppArgb; } using (var newImage = new Bitmap(newWidth, newHeight, imagePixelFormat)) { // Draws the image in the specified size with quality mode set to HighQuality using (Graphics graphics = Graphics.FromImage(newImage)) { graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; using (var imageAttributes = new ImageAttributes()) { // See https://stackoverflow.com/a/11850971/7442826 // Fixes the 50% gray border issue on bright white or dark images imageAttributes.SetWrapMode(WrapMode.TileFlipXY); // In addition to possibly scaling, we want PNG images to have transparent backgrounds. if (!appearsToBeJpeg) { // This specifies that all white or very-near-white pixels (all color components at least 253/255) // will be made transparent. imageAttributes.SetColorKey(Color.FromArgb(253, 253, 253), Color.White); } var destRect = new Rectangle(0, 0, newWidth, newHeight); graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, imageAttributes); } } // Save the file in the same format as the original, and return its bytes. using (var tempFile = TempFile.WithExtension(Path.GetExtension(filePath))) { // This uses default quality settings for jpgs...one site says this is // 75 quality on a scale that runs from 0-100. For most images, this // should give a quality barely distinguishable from uncompressed and still save // about 7/8 of the file size. Lower quality settings rapidly lose quality // while only saving a little space; higher ones rapidly use more space // with only marginal quality improvement. // See https://photo.stackexchange.com/questions/30243/what-quality-to-choose-when-converting-to-jpg // for more on quality and https://docs.microsoft.com/en-us/dotnet/framework/winforms/advanced/how-to-set-jpeg-compression-level // for how to control the quality setting if we decide to (RobustImageIO has // suitable overloads). RobustImageIO.SaveImage(newImage, tempFile.Path, image.RawFormat); // Copy the metadata from the original file to the new file. var metadata = SIL.Windows.Forms.ClearShare.Metadata.FromFile(filePath); if (!metadata.IsEmpty) { metadata.Write(tempFile.Path); } return(RobustFile.ReadAllBytes(tempFile.Path)); } } } } return(RobustFile.ReadAllBytes(filePath)); }
private bool MakePngBackgroundTransparent(string originalPath, string pathToProcessedImage) { try { using (var originalImage = PalasoImage.FromFileRobustly(originalPath)) { //if it's a jpeg, we don't resize, we don't mess with transparency, nothing. These things //are scary in .net. Just send the original back and wash our hands of it. if (ImageUtils.AppearsToBeJpeg(originalImage)) { return(false); } using (var processedBitmap = MakePngBackgroundTransparent(originalImage)) { //Hatton July 2012: //Once or twice I saw a GDI+ error on the Save below, when the app 1st launched. //I verified that if there is an IO error, that's what you get (a GDI+ error). //I looked once, and the %temp%/Bloom directory wasn't there, so that's what I think caused the error. //It's not clear why the temp/bloom directory isn't there... possibly it was there a moment ago //but then some startup thread cleared and deleted it? (we are now running on a thread responding to the http request) Exception error = null; for (var i = 0; i < 3; i++) //try three times { try { error = null; RobustImageIO.SaveImage(processedBitmap, pathToProcessedImage, originalImage.Image.RawFormat); break; } catch (Exception e) { Logger.WriteEvent("***Error in RuntimeImageProcessor while trying to write image."); Logger.WriteEvent(e.Message); error = e; //in setting the sleep time, keep in mind that this may be one of 20 images //so if the problem happens to all of them, then you're looking 20*retries*sleep-time, //which will look like hung program. //Meanwhile, this transparency thing is actually just a nice-to-have. If we give //up, it's ok. Thread.Sleep(100); //wait a 1/5 second before trying again } } if (error != null) { throw error; //will be caught below } } } return(true); } //we want to gracefully degrade if this fails (as it did once, see comment in bl-2871) catch (TagLib.CorruptFileException e) { NonFatalProblem.Report(ModalIf.Beta, PassiveIf.All, "Problem with image metadata", originalPath, e); return(false); } catch (Exception e) { //while beta might make sense, this is actually //a common failure at the moment, with the license.png //so I'm setting to alpha. NonFatalProblem.Report(ModalIf.Alpha, PassiveIf.All, "Problem making image transparent.", originalPath, e); return(false); } }
// Make a thumbnail of the input image. newWidth and newHeight are both limits; the image will not be larger than original, // but if necessary will be shrunk to fit within the indicated rectangle. public static bool GenerateEBookThumbnail(string coverImagePath, string pathToProcessedImage, int thumbnailWidth, int thumbnailHeight, Color backColor) { using (var coverImage = PalasoImage.FromFileRobustly(coverImagePath)) { var coverImageWidth = coverImage.Image.Width; var coverImageHeight = coverImage.Image.Height; // We want to see a small border of background color, even if the image is a photo. const int kborder = 1; var availableThumbnailWidth = thumbnailWidth - (2 * kborder); var availableThumbnailHeight = thumbnailHeight - (2 * kborder); // Calculate how big the image can be while keeping its original proportions. // First assume the width is the limiting factor var targetImageWidth = (coverImageWidth > availableThumbnailWidth) ? availableThumbnailWidth : coverImage.Image.Width; var targetImageHeight = targetImageWidth * coverImageHeight / coverImageWidth; // if actually the height is the limiting factor, maximize height and re-compute the width if (targetImageHeight > availableThumbnailHeight) { targetImageHeight = availableThumbnailHeight; targetImageWidth = targetImageHeight * coverImageWidth / coverImageHeight; } // pad to center the cover image var horizontalPadding = (availableThumbnailWidth - targetImageWidth) / 2; var verticalPadding = (availableThumbnailHeight - targetImageHeight) / 2; var destRect = new Rectangle(kborder + horizontalPadding, kborder + verticalPadding, targetImageWidth, targetImageHeight); // the decision here is just a heuristic based on the observation that line-drawings seem to look better in nice square block of color, // while full-color (usually jpeg) books look better with a thin (or no) border. We could put this under user control eventually. Rectangle backgroundAndBorderRect; var appearsToBeJpeg = ImageUtils.AppearsToBeJpeg(coverImage); if (appearsToBeJpeg) { backgroundAndBorderRect = destRect; backgroundAndBorderRect.Inflate(kborder * 2, kborder * 2); } else { // or, if we decide to always deliver the full thing: backgroundAndBorderRect = new Rectangle(0, 0, thumbnailWidth, thumbnailHeight); } using (var thumbnail = new Bitmap(thumbnailWidth, thumbnailHeight)) using (var g = Graphics.FromImage(thumbnail)) using (var brush = new SolidBrush(backColor)) { g.FillRectangle(brush, backgroundAndBorderRect); lock (ConvertWhiteToTransparent) { var imageAttributes = appearsToBeJpeg ? null : ConvertWhiteToTransparent; g.DrawImage( coverImage.Image, // finally, draw the cover image destRect, // with a scaled and centered destination 0, 0, coverImageWidth, coverImageHeight, // from the entire cover image, GraphicsUnit.Pixel, imageAttributes); // changing white to transparent if a png } RobustImageIO.SaveImage(thumbnail, pathToProcessedImage); // PNG thumbnails created from jpeg files seem to often be way too big, so try to save them as jpeg // files instead if it saves space. See https://silbloom.myjetbrains.com/youtrack/issue/BL-5605. if (appearsToBeJpeg && Path.GetFileName(pathToProcessedImage) == "thumbnail.png") { var jpgPath = Path.ChangeExtension(pathToProcessedImage, "jpg"); RobustImageIO.SaveImage(thumbnail, jpgPath, ImageFormat.Jpeg); var infoPng = new FileInfo(pathToProcessedImage); var infoJpg = new FileInfo(jpgPath); //Debug.WriteLine(String.Format("thumbnail.png size={0}; thumbnail.jpg size={1} (using smaller)", infoPng.Length, infoJpg.Length)); if (infoJpg.Length < infoPng.Length) { File.Delete(pathToProcessedImage); } else { File.Delete(jpgPath); } } } } return(true); }