PNG size growth when using RenderTargetBitmap and PngBitmapEncoder - c#

I'm using a method to merge some tile images into a single image. But if I apply it to four 30kb- PNG images, the resulting PNG image would be 500K (~4x more than what I expect)
This is some part of the code that I'm using (suggested by Cédric Bignon):
BitmapFrame frame1 = BitmapDecoder.Create(new Uri(path1), BitmapCreateOptions.None, BitmapCacheOption.OnLoad).Frames.First();
BitmapFrame frame2 = BitmapDecoder.Create(new Uri(path2), BitmapCreateOptions.None, BitmapCacheOption.OnLoad).Frames.First();
BitmapFrame frame3 = BitmapDecoder.Create(new Uri(path3), BitmapCreateOptions.None, BitmapCacheOption.OnLoad).Frames.First();
BitmapFrame frame4 = BitmapDecoder.Create(new Uri(path4), BitmapCreateOptions.None, BitmapCacheOption.OnLoad).Frames.First();
// Gets the size of the images (I assume each image has the same size)
int imageWidth = frame1.PixelWidth;
int imageHeight = frame1.PixelHeight;
// Draws the images into a DrawingVisual component
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(frame1, new Rect(0, 0, imageWidth, imageHeight));
drawingContext.DrawImage(frame2, new Rect(imageWidth, 0, imageWidth, imageHeight));
drawingContext.DrawImage(frame3, new Rect(0, imageHeight, imageWidth, imageHeight));
drawingContext.DrawImage(frame4, new Rect(imageWidth, imageHeight, imageWidth, imageHeight));
}
// Converts the Visual (DrawingVisual) into a BitmapSource
RenderTargetBitmap bmp = new RenderTargetBitmap(imageWidth * 2, imageHeight * 2, 96, 96, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
// Creates a PngBitmapEncoder and adds the BitmapSource to the frames of the encoder
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
// Saves the image into a file using the encoder
using (Stream stream = File.OpenWrite(pathTileImage))
encoder.Save(stream);
Anyone knows what is going on?

It's difficult to say without knowing the original images. PNG supports several color models and compressions. If, for example, the 4 original images have (different) palettes, and the composition will have to resort to a true colour format, and the total size will probably be way more than 4 times the original sizes.

Related

Saving DrawingVisual to an image file with a given DPI

I have got a WPF application, and I would like to save a Canvas to a file with a correct DPI value. The Canvas size is the real physical size, eg. 20x10 cm, at 300 DPI, so it's 2362x1181.
I draw images and shapes with DrawingVisual, then I create a RenderTargetBitmap. The size of the rtb is the size of the Canvas. The dpiX and dpiY are 96. I got correct image resolution only with 96 DPI. When I set it to 300, the canvas become upscale, and cropped to 2362x1181. So, it's not good. I tried to modify the canvas width and height value by dpi factor, but didn't work either.
After the RenderTargetBitmap, I use BitmapEncoder, BitmapFrame, and BinaryWriter. See code below. Working great, but the image DPI value will be 96. I've read a tons of topics about reading DPI, resaving image, using SetResolution, but I don't want to loose quality, I don't want to read file and resave it, I don't want to change pixel width/height, etc.
I just really want to save a DrawingVisual with a given DPI. I could write EXIF data for "X Resolution" (uint=282) and "Y Resoulution" (uint=283), but that didn't affect the Image DPI settings, so eg. Photoshop will read 96, not 300.
BitmapEncoder encoder = new JpegBitmapEncoder();
BitmapFrame bFrame = BitmapFrame.Create(rtb, null, meta, icc);
encoder.Frames.Add(bFrame);
using (var stream = new MemoryStream())
{
encoder.Save(stream);
byte[] imageData = stream.ToArray();
using (FileStream fs = new FileStream(path, ...)
{
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(imageData);
bw.Close();
}
}
The following method saves a UIElement to a JPEG file with the specified DPI value:
public static void SaveElement(UIElement element, double dpi, string path)
{
var visual = new DrawingVisual();
var width = element.RenderSize.Width;
var height = element.RenderSize.Height;
using (var context = visual.RenderOpen())
{
context.DrawRectangle(new VisualBrush(element), null,
new Rect(0, 0, width, height));
}
var bitmap = new RenderTargetBitmap(
(int)(width * dpi / 96), (int)(height * dpi / 96),
dpi, dpi, PixelFormats.Default);
bitmap.Render(visual);
var encocer = new JpegBitmapEncoder();
encocer.Frames.Add(BitmapFrame.Create(bitmap));
using (var stream = File.OpenWrite(path))
{
encocer.Save(stream);
}
}
Alternatively, apply the DPI scaling to the DrawingVisual:
public static void SaveElement(UIElement element, double dpi, string path)
{
var visual = new DrawingVisual();
var width = element.RenderSize.Width;
var height = element.RenderSize.Height;
using (var context = visual.RenderOpen())
{
context.DrawRectangle(new VisualBrush(element), null,
new Rect(0, 0, width / dpi * 96, height / dpi * 96));
}
var bitmap = new RenderTargetBitmap(
(int)width, (int)height, dpi, dpi, PixelFormats.Default);
bitmap.Render(visual);
var encocer = new JpegBitmapEncoder();
encocer.Frames.Add(BitmapFrame.Create(bitmap));
using (var stream = File.OpenWrite(path))
{
encocer.Save(stream);
}
}
It seems the solution is the Bitmap SetResolution, after all. I tested it, and it looks like not affect the image quality after the JpegBitmapEncoder()! And the image resolution is untouched, keep the metadata, only the DPI will change!
Helped this documentation: https://learn.microsoft.com/en-us/dotnet/framework/winforms/advanced/how-to-set-jpeg-compression-level
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.QualityLevel = 100;
BitmapFrame bFrame = BitmapFrame.Create(rtb, null, meta, icc);
encoder.Frames.Add(bFrame);
using (var stream = new MemoryStream())
{
encoder.Save(stream);
using (var bmpOutput = new System.Drawing.Bitmap(stream))
{
System.Drawing.Imaging.ImageCodecInfo myEncoder = GetEncoder(ImageFormat.Jpeg);
var encoderParameters = new System.Drawing.Imaging.EncoderParameters(1);
encoderParameters.Param[0] = new System.Drawing.Imaging.EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
bmpOutput.SetResolution(300.0f, 300.0f);
bmpOutput.Save(filePath, myEncoder, encoderParameters);
}
}
I tried to apply DPI scaling to the DrawingVisual, but it produced less image quality. :( Also I tried to set the canvas to 756x378 (the 96 DPI size), then set the RenderTargetBitmap to 2362x1181 and 300 DPI, but that produced less image quality too. So, it seems any downscale then upscale or upscale then downscale are not the best solution. The DrawingVisual must be in the final render size.

WPF .png overlay using DrawingVisual

I'm using this example - I found around here - to overlay two .png images and then save the result as a third .png image.
The input images are:
The output image should (in my dreams) be:
And instead I get this:
Here is the code:
public static void Test()
{
// Loads the images to tile (no need to specify PngBitmapDecoder, the correct decoder is automatically selected)
BitmapFrame frame1 = BitmapDecoder.Create(new Uri(#"D:\_tmp_\MaxMara\Test\Monoscope.png"), BitmapCreateOptions.None, BitmapCacheOption.OnLoad).Frames.First();
BitmapFrame frame2 = BitmapDecoder.Create(new Uri(#"D:\_tmp_\MaxMara\Test\OverlayFrame.png"), BitmapCreateOptions.None, BitmapCacheOption.OnLoad).Frames.First();
// Gets the size of the images (I assume each image has the same size)
int imageWidth = 1920;
int imageHeight = 1080;
// Draws the images into a DrawingVisual component
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(frame1, new Rect(0, 0, imageWidth, imageHeight));
drawingContext.DrawImage(frame2, new Rect(0, 0, imageWidth, imageHeight));
}
// Converts the Visual (DrawingVisual) into a BitmapSource
RenderTargetBitmap bmp = new RenderTargetBitmap(imageWidth, imageHeight, 300, 300, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
// Creates a PngBitmapEncoder and adds the BitmapSource to the frames of the encoder
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
// Saves the image into a file using the encoder
using (Stream stream = File.Create(#"D:\_tmp_\MaxMara\Test\Result.png"))
encoder.Save(stream);
}
Note: if i use 100 dpi as in:
RenderTargetBitmap bmp = new RenderTargetBitmap(imageWidth, imageHeight, 100, 100, PixelFormats.Pbgra32);
I get the correct result (meaning: the result I want).
I don't understand why. All images are 300 DPI
Can anyone shed some light on the topic please?
Thank you for your time
Orf
Do not use the PixelWidth and PixelHeight (i.e. your imageWidth and imageHeight values) of the bitmaps for drawing them into a DrawingContext.
Use their Width and Height values instead, because these give the bitmap size in device-independent units (1/96th inch per unit) as required for drawing.
using (var drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(frame1, new Rect(0, 0, frame1.Width, frame1.Height));
drawingContext.DrawImage(frame2, new Rect(0, 0, frame2.Width, frame2.Height));
}

How do I correctly transform an image in WPF?

I want to write program that functions something like PhotoShop.
1.upload image
2. Then I want to do skew transform, But when I do transform I have a problem, my picture goes beyond the edge of the workspace.
How do I transform without this problem(I think I should create a new Image every time I do a transform).
Then I crop, but crop makes the picture without transform. I think if I create a new image every time I do a transform, the problem will be fixed.
How do I do this correctly?
How do I correctly create this image in WPF? How to do transform and save an image? I am using(System.Drawing.Bitmap, System.Windows.Media.Imaging) Maybe, can someone show me experiences, code or useful material?
For the skew tranformation you may use MatrixTranform. Basic idea is described here
Below is the code that transforms an image located at "D:\input.png", attaches transformation result to the source of Image that defined in the .xaml file:
<Image Name="imgProcess" />
and writes result to the file "D:\skew.png"
double skewX = .0;
double skewY = Math.Tan(Math.PI / 18);
MatrixTransform transformation = new MatrixTransform(1, skewY, skewX, 1, 0, 0)
BitmapImage image = new BitmapImage(new Uri(#"D:\input.png"));
var boundingRect = new Rect(0, 0, image.Width + image.Height * skewX, image.Height + image.Width * skewY);
DrawingGroup dGroup = new DrawingGroup();
using (DrawingContext dc = dGroup.Open())
{
dc.PushTransform(transformation);
dc.DrawImage(image, boundingRect);
}
DrawingImage imageSource = new DrawingImage(dGroup);
imgProcess.Source = imageSource;
SaveDrawingToFile(ToBitmapSource(imageSource), #"D:\skew.png", (int)boundingRect.Width, (int)boundingRect.Height);
private BitmapSource ToBitmapSource(DrawingImage source)
{
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
drawingContext.DrawImage(source, new Rect(new Point(0, 0), new Size(source.Width, source.Height)));
drawingContext.Close();
RenderTargetBitmap bmp = new RenderTargetBitmap((int)source.Width, (int)source.Height, 96, 96, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
return bmp;
}
private void SaveDrawingToFile(BitmapSource image, string fileName, int width, int height)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(image));
using (var stream = new FileStream(fileName, FileMode.Create))
{
encoder.Save(stream);
}
}
Results

read jpg, resize, save as png in WPF application c# or vb.net

Going bonkers here trying to do this in WPF (with all the new image manipulation tools), but can't seem to find a working solution. So far, all the solutions are drawing them on the screen or making multiple saves, but I need to do this completely in memory.
Basically, I want to load a large jpeg into memory, resize it smaller (in memory), save as a small PNG file.
I can load the jpeg file into a BitMap object, fine. after that, I'm stumped.
I found this function that looks like it does the trick but it requires an ImageSource (unfortunately, I can't find a way to convert my in-memory BitMap object to an ImageSource that does not yield a NULL exception.)
private static BitmapFrame CreateResizedImage(ImageSource source, int width, int height, int margin)
{
dynamic rect = new Rect(margin, margin, width - margin * 2, height - margin * 2);
dynamic #group = new DrawingGroup();
RenderOptions.SetBitmapScalingMode(#group, BitmapScalingMode.HighQuality);
#group.Children.Add(new ImageDrawing(source, rect));
dynamic drawingVisual = new DrawingVisual();
using (drawingContext == drawingVisual.RenderOpen())
{
drawingContext.DrawDrawing(#group);
}
// Resized dimensions
// Default DPI values
dynamic resizedImage = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Default);
// Default pixel format
resizedImage.Render(drawingVisual);
return BitmapFrame.Create(resizedImage);
}
With WPF it's as easy as this:
private void ResizeImage(string inputPath, string outputPath, int width, int height)
{
var bitmap = new BitmapImage();
using (var stream = new FileStream(inputPath, FileMode.Open))
{
bitmap.BeginInit();
bitmap.DecodePixelWidth = width;
bitmap.DecodePixelHeight = height;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
}
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using (var stream = new FileStream(outputPath, FileMode.Create))
{
encoder.Save(stream);
}
}
You may consider to only set one of DecodePixelWidth and DecodePixelHeight in order to preserve the original image's aspect ratio.

Resize bitmap image

I want to have smaller size at image saved.
How can I resize it?
I use this code for redering the image:
Size size = new Size(surface.Width, surface.Height);
surface.Measure(size);
surface.Arrange(new Rect(size));
// Create a render bitmap and push the surface to it
RenderTargetBitmap renderBitmap =
new RenderTargetBitmap(
(int)size.Width,
(int)size.Height, 96d, 96d,
PixelFormats.Default);
renderBitmap.Render(surface);
BmpBitmapEncoder encoder = new BmpBitmapEncoder();
// push the rendered bitmap to it
encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
// save the data to the stream
encoder.Save(outStream);
public static Bitmap ResizeImage(Bitmap imgToResize, Size size)
{
try
{
Bitmap b = new Bitmap(size.Width, size.Height);
using (Graphics g = Graphics.FromImage((Image)b))
{
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(imgToResize, 0, 0, size.Width, size.Height);
}
return b;
}
catch
{
Console.WriteLine("Bitmap could not be resized");
return imgToResize;
}
}
The shortest way to resize a Bitmap is to pass it to a Bitmap-constructor together with the desired size (or width and height):
bitmap = new Bitmap(bitmap, width, height);
Does your "surface" visual have scaling capability? You can wrap it in a Viewbox if not, then render the Viewbox at the size you want.
When you call Measure and Arrange on the surface, you should provide the size you want the bitmap to be.
To use the Viewbox, change your code to something like the following:
Viewbox viewbox = new Viewbox();
Size desiredSize = new Size(surface.Width / 2, surface.Height / 2);
viewbox.Child = surface;
viewbox.Measure(desiredSize);
viewbox.Arrange(new Rect(desiredSize));
RenderTargetBitmap renderBitmap =
new RenderTargetBitmap(
(int)desiredSize.Width,
(int)desiredSize.Height, 96d, 96d,
PixelFormats.Default);
renderBitmap.Render(viewbox);

Categories