Save content of a visual Object as a image file in WPF? - wpf

I need to save the content of a WPF Object as an Image file. In my application I have a chart drawn on a Canvas object. This is what I need to save. The Canvas with all child objects.

What you're looking for is the RenderTargetBitmap class. There's an example of its use on the MSDN page I linked, and there's another good example that includes saving to a file here:
RenderTargetBitmap by Eric Sinc

Here is the func which creates RenderTargetBitmap object, that will be used in further funcs.
public static RenderTargetBitmap ConvertToBitmap(UIElement uiElement, double resolution)
{
var scale = resolution / 96d;
uiElement.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
var sz = uiElement.DesiredSize;
var rect = new Rect(sz);
uiElement.Arrange(rect);
var bmp = new RenderTargetBitmap((int)(scale * (rect.Width)), (int)(scale * (rect.Height)), scale * 96, scale * 96, PixelFormats.Default);
bmp.Render(uiElement);
return bmp;
}
This functionc creates JPEG string content of file and writes it to a file:
public static void ConvertToJpeg(UIElement uiElement, string path, double resolution)
{
var jpegString = CreateJpeg(ConvertToBitmap(uiElement, resolution));
if (path != null)
{
try
{
using (var fileStream = File.Create(path))
{
using (var streamWriter = new StreamWriter(fileStream, Encoding.Default))
{
streamWriter.Write(jpegString);
streamWriter.Close();
}
fileStream.Close();
}
}
catch (Exception ex)
{
//TODO: handle exception here
}
}
}
This function used above to create JPEG string representation of Image content:
public static string CreateJpeg(RenderTargetBitmap bitmap)
{
var jpeg = new JpegBitmapEncoder();
jpeg.Frames.Add(BitmapFrame.Create(bitmap));
string result;
using (var memoryStream = new MemoryStream())
{
jpeg.Save(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
using (var streamReader = new StreamReader(memoryStream, Encoding.Default))
{
result = streamReader.ReadToEnd();
streamReader.Close();
}
memoryStream.Close();
}
return result;
}
Hope this helps.

With the help of the Eric Sinc tutorial I came to the following solution:
It uses a win32 SaveDialog to choose where the file should go and a PngBitmapEncoder (many other BitmapEncoders available!) to convert it to something we can save.
Note that the element being saved in this example is "cnvClasses" and that the size of the output is, quite deliberately, the same as the control.
SaveFileDialog svDlg = new SaveFileDialog();
svDlg.Filter = "PNG files|*.png|All Files|*.*";
svDlg.Title = "Save diagram as PNG";
if (svDlg.ShowDialog().Value == true)
{
RenderTargetBitmap render = new RenderTargetBitmap((int)this.cnvClasses.ActualWidth, (int)this.cnvClasses.ActualHeight, 96, 96, PixelFormats.Pbgra32);
render.Render(cnvClasses);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(render));
using (FileStream fs = new FileStream(svDlg.FileName, FileMode.Create))
encoder.Save(fs);
}

Related

How to optimally find the smallest image rendered by a WPF control

I am using the FormulaControl from WPF-Math to render a bitmap for a tek equation. The bitmap will be delivered as content over a web service ( slack ). There is no desktop component. I am only using the WPF framework to try to capture the image from the tek control. The code for the renderer component is
public static class Renderer
{
private static readonly StaTaskScheduler _StaTaskScheduler = new StaTaskScheduler( 1 );
public static async Task<string> GenerateImage(string formula)
{
string Build()
{
var control = new FormulaControl
{
Formula = formula
, Background = Brushes.White
};
control.Measure(new Size(300, 300));
control.Arrange(new Rect(new Size(300, 300)));
var bmp = new RenderTargetBitmap(300, 300, 96, 96, PixelFormats.Pbgra32);
bmp.Render(control);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bmp));
var file = #"test.png";
using (Stream stm = File.Create(file))
encoder.Save(stm);
return file;
}
return await Task.Factory.StartNew
( Build, CancellationToken.None, TaskCreationOptions.None, _StaTaskScheduler );
}
}
Using the above code and the input
k_{n+1} = n^2 + k_n^2 - k_{n-1}
the below image is generated
As you can see, in this case, an arbitrary size of 300x300 is too big and for a different tek input it maybe too small.
The challenge is to generate a bitmap of exactly the correct size for the rendered equation. How can this be done?
One solution is to render to a large bitmap then auto crop the whitespace. There is a solution for auto cropping whitespace at
Cropping whitespace from image in C#
Using the ImageCrop class from above I modified the rendering code to
public static class Renderer
{
private static readonly StaTaskScheduler _StaTaskScheduler = new StaTaskScheduler( 1 );
public static Bitmap Convert( RenderTargetBitmap inmap )
{
MemoryStream stream = new MemoryStream();
BitmapEncoder encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(inmap));
encoder.Save(stream);
Bitmap bitmap = new Bitmap(stream);
return bitmap;
}
public static async Task<string> GenerateImage(string formula)
{
string Build()
{
var control = new FormulaControl
{
Formula = formula
, Background = Brushes.White
};
control.Measure(new Size(300, 300));
control.Arrange(new Rect(new Size(300, 300)));
var bmp = new RenderTargetBitmap(300, 300, 96, 96, PixelFormats.Pbgra32);
bmp.Render(control);
var image = ImageCrop.AutoCrop( Convert( bmp ) );
var file = #"test.png";
image.Save( file,ImageFormat.Png );
return file;
}
return await Task.Factory.StartNew
( Build, CancellationToken.None, TaskCreationOptions.None, _StaTaskScheduler );
}
}
The output in my slackbot is now perfectly cropped.
Obviously this is not perfect as there is an upper bound on the render size.

Creating bitmap in memory using RenderTargetBitmap fails in production WCF Service

I am using WPF objects to generate an bitmap image in memory. The program that does this resides in a WCF web service. The image renders correctly when I run on locally on IISExpress, and on a test IIS 7 server. However, when running on a server used by QA, the image is not rendered correctly. More specifically, only the top 22px lines of a 250px height image are rendered. The settings on both the test server and the QA server are supposed to be identical (insert skeptical face here).
Question: What possible settings in IIS could be effecting this image rendering? Also, I'm thinking there could possibly be a threading issue since RenderTargetBitmap renders asynchronously, and I do get a partial image.
Here is the code I'm using:
private byte[] RenderGauge(ViewData viewData)
{
double resolution = 4 * ReSize;
double dpi = 96 * resolution;
var view = new Gauge();
var vm = new GuageViewModel(viewData);
view.Measure(new Size(350, 70));
view.Arrange(new Rect(new Size(350, 70)));
var bounds = VisualTreeHelper.GetDescendantBounds(view);
if (bounds != Rect.Empty)
{
height = (int)(Math.Floor(bounds.Height) + 1);
width = (int)(Math.Floor(bounds.Width) + 1);
size = new Size(width, height);
}
var bitmap = new RenderTargetBitmap((int)(width * resolution), (int)(height * resolution), dpi, dpi, PixelFormats.Pbgra32);
var visual = new DrawingVisual();
using (var context = visual.RenderOpen())
{
var brush = new VisualBrush(view);
context.DrawRectangle(brush, null, new Rect(new Point(), bounds.Size));
}
bitmap.Render(visual);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
byte[] img;
using (var MS = new MemoryStream())
{
encoder.Save(MS);
img = MS.ToArray();
}
img = img == null ? new byte[0] : img;
return img;
}
So, I'm doing exactly the same thing and I had a number of issues rendering files. I've found that using a binding to a bitmap in the XAML helps. The code from my view model that returns the image source is:
public Uri ImageUri
{
get { return new Uri(ImagePath, UriKind.Absolute); }
}
public BitmapImage ImageSource
{
get
{
try
{
if (string.IsNullOrEmpty(ImagePath) || !File.Exists(ImagePath))
return null;
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = ImageUri;
image.EndInit();
return image;
}
catch (Exception e)
{
var logger = LogManager.GetLogger(typeof(ImageDetails));
ExceptionHelper.LogExceptionMessage(logger, e);
}
return null;
}
}
Then in the XAML I bind to the ImageSource property.
I think that most problems with RenderTargetBitmap are related to asynchronous bindings in the XAML becauses the render method is synchronous.

Error converting clipboard image

Using WPF 4.5
private Bitmap BitmapFromSource(BitmapSource bitmapsource)
{
Bitmap bitmap;
using (var outStream = new MemoryStream())
{
BitmapEncoder enc = new BmpBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(bitmapsource));
enc.Save(outStream);
bitmap = new Bitmap(outStream);
}
return bitmap;
}
}
and then later:
if (Clipboard.ContainsImage())
{
var bitmapSouce = Clipboard.GetImage();
var bitmap = BitmapFromSource(bitmapSouce);
var tmp = Path.GetTempFileName();
bitmap.Save(tmp, ImageFormat.Png);
...
bitmap.Save() throws an ExternalException, "A generic error in GDI+"
Is it really so hard to save a clipboard image to disk?
It is not necessary to create a System.Drawing.Bitmap (which is WinForms) from a WPF BitmapSource just for saving it.
You could as well directly save to a FileStream:
private void SaveBitmap(BitmapSource bitmapSource, string fileName)
{
var enc = new PngBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(bitmapSource));
using (var outStream = new FileStream(fileName, FileMode.Create))
{
enc.Save(outStream);
}
}
...
var bitmapSource = Clipboard.GetImage();
var tmp = Path.GetTempFileName();
SaveBitmap(bitmapSource, tmp);

Add TransformGroup to a FramworkElement when rendering WPF to a PNG

I've got an app that turns some XAML Usercontrols into PNGs - this has worked really well up to now, unfortunately I now need to double the size of the images.
My method (that doesn't work!) was to add a ScaleTransform to the visual element after I've loaded it ...
This line is the new line at the top of the SaveUsingEncoder method.
visual.RenderTransform = GetScaleTransform(2);
The PNG is the new size (3000 x 2000) - but the XAML is Rendered at 1500x1000 in the centre of the image.
Can anyone assist please?
private void Load(string filename)
{
var stream = new FileStream(filename), FileMode.Open);
var frameworkElement = (FrameworkElement)(XamlReader.Load(stream));
var scale = 2;
var encoder = new PngBitmapEncoder();
var availableSize = new Size(1500 * scale, 1000 * scale);
frameworkElement.Measure(availableSize);
frameworkElement.Arrange(new Rect(availableSize));
name = name.Replace(" ", "-");
SaveUsingEncoder(frameworkElement, string.Format(#"{0}.png", name), encoder, availableSize);
}
private TransformGroup GetScaleTransform(int scale)
{
var myScaleTransform = new ScaleTransform {ScaleY = scale, ScaleX = scale};
var myTransformGroup = new TransformGroup();
myTransformGroup.Children.Add(myScaleTransform);
return myTransformGroup;
}
private void SaveUsingEncoder(FrameworkElement visual, string fileName, BitmapEncoder encoder, Size size)
{
visual.RenderTransform = GetScaleTransform(2);
var bitmap = new RenderTargetBitmap(
(int) size.Width,
(int) size.Height,
96,
96,
PixelFormats.Pbgra32);
bitmap.Render(visual);
var frame = BitmapFrame.Create(bitmap);
encoder.Frames.Add(frame);
using (var stream = File.Create(fileName))
{
encoder.Save(stream);
}
}
Called visual.UpdateLayout before rendering into the RenderTargetBitmap
(Thanks to Clemens for this answer - but he put it as a comment!)

Easiest way of saving wpf Image control to a file

I have a Image control inside my wpf application, which has a large image inside of it, but the control itself is only 60x150, this means it only shows a certain portion of this image.
What is the easiest way of saving the visible portion to a file?
Thank you for your help.
[EDIT]
I ended up using code found here (which I've not been able to locate before posting here)...
Grid r = new Grid();
r.Background = new ImageBrush(image2.Source);
System.Windows.Size sz = new System.Windows.Size(image2.Source.Width, image2.Source.Height);
r.Measure(sz);
r.Arrange(new Rect(sz));
RenderTargetBitmap rtb = new RenderTargetBitmap((int)image2.Source.Width, (int)image2.Source.Height, 96d, 96d, PixelFormats.Default);
rtb.Render(r);
BmpBitmapEncoder encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
FileStream fs = File.Open(#"C:\lol.png", FileMode.Create);
encoder.Save(fs);
fs.Close();
You could use RenderTargetBitmap class and BitmapEncoder.
Define these methods:
void SaveToBmp(FrameworkElement visual, string fileName)
{
var encoder = new BmpBitmapEncoder();
SaveUsingEncoder(visual, fileName, encoder);
}
void SaveToPng(FrameworkElement visual, string fileName)
{
var encoder = new PngBitmapEncoder();
SaveUsingEncoder(visual, fileName, encoder);
}
// and so on for other encoders (if you want)
void SaveUsingEncoder(FrameworkElement visual, string fileName, BitmapEncoder encoder)
{
RenderTargetBitmap bitmap = new RenderTargetBitmap((int)visual.ActualWidth, (int)visual.ActualHeight, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(visual);
BitmapFrame frame = BitmapFrame.Create(bitmap);
encoder.Frames.Add(frame);
using (var stream = File.Create(fileName))
{
encoder.Save(stream);
}
}
If you have your Image control inside a container like this:
<Grid x:Name="MyGrid">
<Image Name="MyImage" Stretch="None"></Image>
</Grid>
You just need to do so:
SaveToPng(MyGrid, "image.png");
Otherwise you can simply pass the dimensions you want when you use RenderTargetBitmap:
SaveToPng(MyImage, "image.png");
...
RenderTargetBitmap bitmap = new RenderTargetBitmap(YourWidth, YourHeight, 96, 96, PixelFormats.Pbgra32);
I ran into the same 'black' image issue that other did when using gliderkite's solution. The 'black' image appears to be due to the FrameworkElement's margin causing it to get rendered outside of the captured image. I found the workaround in a comment on Rick Stahl's blog
Specifically, measuring and arranging prior to rendering gives it a chance to adjust itself to the fact there are no margins in the picture. The following is a static class I now re-use for screen captures. This is based of of gliderkite's answer and the information on Rick Stahl's blog.
public static class ScreenCapture
{
public static void SaveToBmp(FrameworkElement visual, string fileName)
{
var encoder = new BmpBitmapEncoder();
SaveUsingEncoder(visual, fileName, encoder);
}
public static void SaveToPng(FrameworkElement visual, string fileName)
{
var encoder = new PngBitmapEncoder();
SaveUsingEncoder(visual, fileName, encoder);
}
public static void SaveToJpeg(FrameworkElement visual, string fileName)
{
var encoder = new JpegBitmapEncoder();
SaveUsingEncoder(visual, fileName, encoder);
}
private static void SaveUsingEncoder(FrameworkElement visual, string fileName, BitmapEncoder encoder)
{
RenderTargetBitmap bitmap = new RenderTargetBitmap((int)visual.ActualWidth, (int)visual.ActualHeight, 96, 96, PixelFormats.Pbgra32);
Size visualSize = new Size(visual.ActualWidth, visual.ActualHeight);
visual.Measure(visualSize);
visual.Arrange(new Rect(visualSize));
bitmap.Render(visual);
BitmapFrame frame = BitmapFrame.Create(bitmap);
encoder.Frames.Add(frame);
using (var stream = File.Create(fileName))
{
encoder.Save(stream);
}
}
}

Resources